Commit on 2025/07/24 周四 17:54:56.38
This commit is contained in:
parent
59de4f24a6
commit
3f0c608c84
@ -365,11 +365,13 @@ LocalDateTime.now(),获取当前时间
|
||||
|
||||
```java
|
||||
public class LambdaExample {
|
||||
|
||||
// 定义函数式接口,doSomething 有两个参数
|
||||
@FunctionalInterface
|
||||
interface MyInterface {
|
||||
void doSomething(int a, int b);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
// 使用匿名内部类实现接口方法
|
||||
MyInterface obj = new MyInterface() {
|
||||
@ -1445,6 +1447,95 @@ public class SomeException extends Exception {
|
||||
|
||||
|
||||
|
||||
## JAVA泛型
|
||||
|
||||
在类、接口或方法定义时,用类型参数来替代具体的类型,编译时检查类型安全,运行时通过类型擦除映射到原始类型。
|
||||
|
||||
### 定义一个泛型类
|
||||
|
||||
```java
|
||||
// 定义一个“盒子”类,可以装任何类型的对象
|
||||
public class Box<T> {
|
||||
private T value;
|
||||
|
||||
public Box() {}
|
||||
|
||||
public Box(T value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public void set(T value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public T get() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
`T` 是类型参数(Type Parameter),可任意命名(常见还有 E、K、V 等)。
|
||||
|
||||
**使用:**
|
||||
|
||||
```java
|
||||
public class Main {
|
||||
public static void main(String[] args) {
|
||||
// 创建一个只装 String 的盒子
|
||||
Box<String> stringBox = new Box<>();
|
||||
stringBox.set("Hello Generics");
|
||||
String s = stringBox.get(); // 自动类型推断为 String
|
||||
System.out.println(s);
|
||||
|
||||
// 创建一个只装 Integer 的盒子
|
||||
Box<Integer> intBox = new Box<>(123);
|
||||
Integer i = intBox.get();
|
||||
System.out.println(i);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 定义一个泛型方法
|
||||
|
||||
有时候我们只想让某个方法支持多种类型,而不必为此写泛型类,就可以在方法前加上类型声明:
|
||||
|
||||
```java
|
||||
public class Utils {
|
||||
//[修饰符] <T> 返回类型 方法名(参数列表) { … }
|
||||
// 泛型方法:打印任意类型的一维数组
|
||||
public static <T> void printArray(T[] array) {
|
||||
for (T element : array) {
|
||||
System.out.println(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
方法签名中 `<T>` 表示这是一个泛型方法,`T` 在参数列表或返回值中使用。
|
||||
|
||||
调用时,编译器会根据传入实参**自动推断** `T`。
|
||||
|
||||
**使用**
|
||||
|
||||
```java
|
||||
public class Main {
|
||||
public static void main(String[] args) {
|
||||
String[] names = {"Alice", "Bob", "Charlie"};
|
||||
Utils.printArray(names);
|
||||
// 等价于 Utils.<String>printArray(names);
|
||||
|
||||
Integer[] nums = {10, 20, 30};
|
||||
Utils.printArray(nums);
|
||||
// 等价于 Utils.<Integer>printArray(nums);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 好用的方法
|
||||
|
||||
### toString()
|
||||
|
108
后端学习/Redis.md
108
后端学习/Redis.md
@ -533,3 +533,111 @@ public void testCommon(){
|
||||
|
||||
|
||||
|
||||
## Redisson
|
||||
|
||||
### 快速入门
|
||||
|
||||
#### 1. 引入依赖
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.redisson</groupId>
|
||||
<artifactId>redisson-spring-boot-starter</artifactId>
|
||||
<version>3.26.0</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
#### 2. 在 application.yml 中配置 Redis 连接(Redisson 会自动读取)
|
||||
```yaml
|
||||
spring:
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
password: # 如无密码则留空
|
||||
database: 0
|
||||
```
|
||||
|
||||
#### 3. 可选:自定义 RedissonClient 配置(无需也可不写)
|
||||
```java
|
||||
@Configuration
|
||||
public class RedissonConfig {
|
||||
@Bean
|
||||
public RedissonClient redissonClient() {
|
||||
org.redisson.config.Config config = new org.redisson.config.Config();
|
||||
config.useSingleServer()
|
||||
.setAddress("redis://localhost:6379")
|
||||
.setDatabase(0);
|
||||
return org.redisson.Redisson.create(config);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. 示例:使用分布式锁防止超卖的 Service
|
||||
```java
|
||||
@Service
|
||||
public class InventoryService {
|
||||
|
||||
private static final String STOCK_KEY_PREFIX = "stock:";
|
||||
private static final String STOCK_LOCK_PREFIX = "lock:stock:";
|
||||
|
||||
private final RedissonClient redissonClient;
|
||||
|
||||
public InventoryService(RedissonClient redissonClient) {
|
||||
this.redissonClient = redissonClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试购买一个单位商品,防止超卖
|
||||
* @param productId 商品 ID
|
||||
* @return 是否扣减成功
|
||||
*/
|
||||
public boolean purchase(String productId) {
|
||||
String lockKey = STOCK_LOCK_PREFIX + productId;
|
||||
RLock lock = redissonClient.getLock(lockKey);
|
||||
boolean success = false;
|
||||
try {
|
||||
// 最多等待 5 秒获取锁,获取后锁自动过期时间 10 秒
|
||||
if (lock.tryLock(5, 10, TimeUnit.SECONDS)) {
|
||||
String stockKey = STOCK_KEY_PREFIX + productId;
|
||||
// 假设库存存储在 Redis 中
|
||||
Integer stock = (Integer) redissonClient.getBucket(stockKey).get();
|
||||
if (stock != null && stock > 0) {
|
||||
redissonClient.getBucket(stockKey).set(stock - 1);
|
||||
success = true;
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
} finally {
|
||||
if (lock.isHeldByCurrentThread()) {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
return success;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 5. 调用示例
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api/order")
|
||||
public class OrderController {
|
||||
|
||||
private final InventoryService inventoryService;
|
||||
|
||||
public OrderController(InventoryService inventoryService) {
|
||||
this.inventoryService = inventoryService;
|
||||
}
|
||||
|
||||
@PostMapping("/buy/{productId}")
|
||||
public ResponseEntity<String> buy(@PathVariable String productId) {
|
||||
boolean ok = inventoryService.purchase(productId);
|
||||
if (ok) {
|
||||
return ResponseEntity.ok("购买成功");
|
||||
} else {
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("库存不足");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -1457,7 +1457,7 @@ feign:
|
||||
|
||||
<img src="https://pic.bitday.top/i/2025/05/26/x2wpbf-0.png" alt="image-20250526200028905" style="zoom:80%;" />
|
||||
|
||||
```
|
||||
```java
|
||||
public class ItemClientFallback implements FallbackFactory<ItemClient> {
|
||||
@Override
|
||||
public ItemClient create(Throwable cause) {
|
||||
|
@ -128,7 +128,7 @@ public class SpringAmqpTest {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
`convertAndSend`如果 2 个参数,第一个表示队列名,第二个表示消息;
|
||||
|
||||
**消息接收**
|
||||
|
||||
@ -151,6 +151,8 @@ public class SpringRabbitListener {
|
||||
|
||||
### 交换机
|
||||
|
||||
无论是 **Direct**、**Topic** 还是 **Fanout** 交换机,你都可以用 **同一个 Binding Key** 把多条队列绑定到**同一个交换机**上。
|
||||
|
||||
**1)fanout:广播给每个绑定的队列**
|
||||
|
||||

|
||||
@ -159,7 +161,7 @@ public class SpringRabbitListener {
|
||||
|
||||
发送消息:
|
||||
|
||||
`convertAndSend`如果2个参数,第一个表示队列名,第二个表示消息;如果3个参数,第一个表示交换机,第二个表示`RoutingKey`,第三个表示消息。
|
||||
`convertAndSend`如果 3 个参数,第一个表示交换机,第二个表示`RoutingKey`,第三个表示消息。
|
||||
|
||||
```java
|
||||
@Test
|
||||
@ -188,15 +190,15 @@ public void testFanoutExchange() {
|
||||
|
||||
**3)Topic交换机**
|
||||
|
||||
`Topic`类型的`Exchange`与`Direct`相比,都是可以根据`RoutingKey`把消息路由到不同的队列。
|
||||
`Topic`类型的交换机与`Direct`相比,都是可以根据`RoutingKey`把消息路由到不同的队列。
|
||||
|
||||
只不过`Topic`类型`Exchange`可以让队列在绑定`BindingKey` 的时候使用**通配符**!
|
||||
只不过`Topic`类型交换机可以让队列在绑定`BindingKey` 的时候使用**通配符**!
|
||||
|
||||
BindingKey一般都是有一个或**多个单词**组成,多个单词之间以`.`分割
|
||||
|
||||
通配符规则:
|
||||
|
||||
- `#`:匹配一个或多个词
|
||||
- `#`:匹配一个或**多个词**
|
||||
- `*`:匹配不多不少恰好**1个词**
|
||||
|
||||
举例:
|
||||
@ -206,9 +208,59 @@ BindingKey一般都是有一个或**多个单词**组成,多个单词之间以
|
||||
|
||||
|
||||
|
||||
转发过程:把发送者传来的 Routing Key 按点分成多级,和各队列的 Binding Key(可以带 `*`、`#` 通配符)做模式匹配,匹配上的队列统统都能收到消息。
|
||||
|
||||
|
||||
|
||||
### Routing Key和Binding Key
|
||||
|
||||
**Routing Key(路由键)**
|
||||
|
||||
- **由发送者(Producer)在发布消息时指定**,附着在消息头上。
|
||||
- 用来告诉交换机:“我的这条消息属于哪类/哪个主题”。
|
||||
|
||||
**Binding Key(绑定键)**
|
||||
|
||||
- **由消费者(在应用启动或队列声明时)指定**,是把队列绑定到交换机时用的规则。**有些 UI 里 Routing Key 等同于 Binding Key!**
|
||||
- 告诉交换机:“符合这个键的消息,投递到我这个队列”。
|
||||
|
||||
|
||||
|
||||
**交换机本身不设置 Routing Key 或 Binding Key**,它只根据类型(Direct/Topic/Fanout/Headers)和已有的“队列–绑定键”关系,把 incoming Routing Key 匹配到对应的队列。
|
||||
|
||||
|
||||
|
||||
**Direct Exchange**
|
||||
|
||||
- **路由规则**:`Routing Key === Binding Key`(完全一致)
|
||||
- 场景:一对一或一对多的精确路由
|
||||
|
||||
**Topic Exchange**
|
||||
|
||||
- 路由规则
|
||||
|
||||
:支持通配符
|
||||
|
||||
- `*`:匹配一个单词
|
||||
- `#`:匹配零个或多个单词
|
||||
|
||||
- 例:
|
||||
|
||||
- **Binding Key**绑定键 `order.*` → 能匹配 `order.created`、`order.paid`
|
||||
- 绑定键 `order.#` → 能匹配 `order.created.success`、`order` 等
|
||||
|
||||
**Fanout Exchange**
|
||||
|
||||
- **路由规则**:忽略 Routing/Binding Key,消息广播到所有绑定队列
|
||||
- 场景:聊天室广播、缓存失效通知等
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### 基于注解声明交换机、队列
|
||||
|
||||
以往我们都在 RabbitMQ 管理控制台手动创建队列和交换机,开发人员还得把所有配置整理一遍交给运维,既繁琐又容易出错。更好的做法是在应用启动时自动检测所需的队列和交换机,若不存在则直接创建。
|
||||
前面都是在 RabbitMQ 管理**控制台手动创建**队列和交换机,开发人员还得把所有配置整理一遍交给运维,既繁琐又容易出错。更好的做法是在应用启动时自动检测所需的队列和交换机,若不存在则直接创建。
|
||||
|
||||
**基于注解方式来声明**
|
||||
|
||||
|
@ -706,6 +706,25 @@ docker images
|
||||
|
||||
|
||||
|
||||
- **端口占用问题**
|
||||
|
||||
Error response from daemon: Ports are not available: exposing port TCP 0.0.0.0:5672 -> 0.0.0.0:0: listen tcp 0.0.0.0:5672: bind: An attempt was made to access a socket in a way forbidden by its access permissions.
|
||||
|
||||
先查看是否端口被占用:
|
||||
|
||||
```shell
|
||||
netstat -aon | findstr 5672
|
||||
```
|
||||
|
||||
如果没有被占用,那么就是windows的bug,在CMD使用管理员权限重启NAT网络服务即可
|
||||
|
||||
```shell
|
||||
net stop winnat
|
||||
net start winnat
|
||||
```
|
||||
|
||||
|
||||
|
||||
- **构建镜像失败**
|
||||
|
||||
```text
|
||||
|
@ -1263,9 +1263,9 @@ https://natapp.cn/#download ,参考官方文档,配置本地token,启动
|
||||
|
||||
|
||||
|
||||
## 自己搭建FRP
|
||||
### 自己搭建FRP
|
||||
|
||||
### 数据流转流程
|
||||
#### 数据流转流程
|
||||
|
||||
**外部请求到达 frp-server**
|
||||
|
||||
@ -1297,7 +1297,7 @@ remote_port= 18080
|
||||
|
||||
- 它会在容器或宿主机内部打开一个新的 TCP 连接,指向 `127.0.0.1:8080`(即你的 Java 服务)
|
||||
|
||||
### frp-server
|
||||
#### frp-server
|
||||
|
||||
frps.toml:
|
||||
|
||||
@ -1331,7 +1331,7 @@ services:
|
||||
network_mode: "host"
|
||||
```
|
||||
|
||||
### frp-client
|
||||
#### frp-client
|
||||
|
||||
frpc.toml:
|
||||
|
||||
|
195
科研/小论文.md
195
科研/小论文.md
@ -12,6 +12,152 @@
|
||||
|
||||
|
||||
|
||||
## 特征值精度预估
|
||||
|
||||
### 1. 噪声随机变量与协方差
|
||||
|
||||
| 符号 | 含义 |
|
||||
| ----- | ------------------------------------ |
|
||||
| $w_i$ | 第 $i$ 个**过程噪声**样本 |
|
||||
| $v_j$ | 第 $j$ 个**观测噪声**样本 |
|
||||
| $Q$ | 过程噪声的真实方差(协方差矩阵退化) |
|
||||
| $R$ | 观测噪声的真实方差(协方差矩阵退化) |
|
||||
|
||||
> **说明**:
|
||||
>
|
||||
> - 在矩阵形式的 Kalman Filter 中,通常写作
|
||||
> $$
|
||||
> w_k\sim\mathcal N(0,Q),\quad v_k\sim\mathcal N(0,R).
|
||||
> $$
|
||||
>
|
||||
> - 这里为做统计检验,把 $w_i, v_j$ 当作样本,$Q,R$ 就是它们在**标量**情况下的方差。
|
||||
|
||||
---
|
||||
|
||||
### 2. 样本统计量
|
||||
|
||||
| 符号 | 含义 |
|
||||
| ----------- | ------------------------------ |
|
||||
| $N_w,\;N_v$ | 过程噪声样本数和观测噪声样本数 |
|
||||
| $\bar w$ | 过程噪声样本均值 |
|
||||
| $\bar v$ | 观测噪声样本均值 |
|
||||
| $s_w^2$ | 过程噪声的**样本方差**估计 |
|
||||
| $s_v^2$ | 观测噪声的**样本方差**估计 |
|
||||
|
||||
> **定义**:
|
||||
> $$
|
||||
> \bar w = \frac1{N_w}\sum_{i=1}^{N_w}w_i,\quad
|
||||
> s_w^2 = \frac1{N_w-1}\sum_{i=1}^{N_w}(w_i-\bar w)^2,
|
||||
> $$
|
||||
>
|
||||
> $$
|
||||
> \bar v = \frac1{N_v}\sum_{j=1}^{N_v}v_j,\quad
|
||||
> s_v^2 = \frac1{N_v-1}\sum_{j=1}^{N_v}(v_j-\bar v)^2.
|
||||
> $$
|
||||
|
||||
---
|
||||
|
||||
### 3. 方差比的 $F$ 分布区间估计
|
||||
|
||||
1. **构造 $F$ 统计量**
|
||||
$$
|
||||
F = \frac{(s_w^2/Q)}{(s_v^2/R)}
|
||||
= \frac{s_w^2}{s_v^2}\,\frac{R}{Q}
|
||||
\sim F(N_w-1,\,N_v-1).
|
||||
$$
|
||||
|
||||
2. **置信区间**(置信度 $1-\alpha$)
|
||||
查得
|
||||
$$
|
||||
F_{L}=F_{\alpha/2}(N_w-1,N_v-1),\quad
|
||||
F_{U}=F_{1-\alpha/2}(N_w-1,N_v-1),
|
||||
$$
|
||||
则
|
||||
$$
|
||||
\begin{align*}
|
||||
P\Big\{F_{\rm L}\le F\le F_{\rm U}\Big\}=1-\alpha \quad\Longrightarrow \quad P\Big\{F_{\rm L}\,\le\frac{s_w^2}{s_v^2}\,\frac{R}{Q}\le F_{\rm U}\,\Big\}=1-\alpha.
|
||||
\end{align*}
|
||||
$$
|
||||
|
||||
3. **解出 $\frac{R}{Q}$ 的区间**
|
||||
$$
|
||||
P\Bigl\{\,F_{L}\,\frac{s_v^2}{s_w^2}\le \frac{R}{Q}\le F_{U}\,\frac{s_v^2}{s_w^2}\Bigr\}=1-\alpha.
|
||||
$$
|
||||
令
|
||||
$$
|
||||
\theta_{\min}=\sqrt{\,F_{L}\,\frac{s_v^2}{s_w^2}\,},\quad
|
||||
\theta_{\max}=\sqrt{\,F_{U}\,\frac{s_v^2}{s_w^2}\,}.
|
||||
$$
|
||||
|
||||
---
|
||||
|
||||
### 4. 卡尔曼增益与误差上界
|
||||
|
||||
在标量情况下(即状态和观测均为1维),卡尔曼增益公式可简化为:
|
||||
|
||||
$$
|
||||
K = \frac{P_k H^T}{HP_k H^T + R} = \frac{HP_k}{H^2 P_k + R}
|
||||
$$
|
||||
|
||||
针对我们研究对象,特征值滤波公式的系数都属于实数域。$P_{k-1}$是由上次迭代产生,因此可以$FP_{k-1}F^T$看作定值,则$P_k$的方差等于$Q$的方差,即:
|
||||
|
||||
$$
|
||||
\text{var}(P_k) = \text{var}(Q)
|
||||
$$
|
||||
|
||||
令 $c = H$, $m = 1/H$(满足 $cm = 1$),则:
|
||||
$$
|
||||
K = \frac{cP_k}{c^2 P_k + R} = \frac{1}{c + m(R/P_k)} \quad R/P_k\in[\theta_{\min}^2,\theta_{\max}^2].
|
||||
$$
|
||||
|
||||
|
||||
|
||||
则极值为
|
||||
$$
|
||||
K_{\max}=\frac{1}{c + m\,\theta_{\min}^2},\quad
|
||||
K_{\min}=\frac{1}{c + m\,\theta_{\max}^2}.
|
||||
$$
|
||||
|
||||
通过历史数据计算预测误差的均值:
|
||||
$$
|
||||
E(x_k' - x_k) \approx \frac{1}{M} \sum_{m=1}^{M} (x_k^{l(m)} - x_k^{(m)})\\
|
||||
$$
|
||||
定义误差上界
|
||||
$$
|
||||
\xi
|
||||
=\bigl(K_{\max}-K_{\min}\bigr)\;E\bigl(x_k'-x_k\bigr)
|
||||
=\Bigl(\tfrac1{c+m\,\theta_{\min}^2}-\tfrac1{c+m\,\theta_{\max}^2}\Bigr)
|
||||
\,E(x_k'-x_k).
|
||||
$$
|
||||
若令 $c\,m=1$,可写成
|
||||
$$
|
||||
\xi
|
||||
=\frac{(\theta_{\max}-\theta_{\min})\,E(x_k'-x_k)}
|
||||
{(c^2+\theta_{\min})(c^2+\theta_{\max})}.
|
||||
$$
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
量化噪声方差估计的不确定性,进而评估卡尔曼滤波器增益的可能波动,并据此给出滤波误差的上界.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -228,3 +374,52 @@ $$
|
||||
- 正确做法是——在时刻 $t$ 得到 $L_t,B_t,S_t$ 后,用上面的直接公式一次算出**所有未来** $\hat x_{t+1},\hat x_{t+2},\dots$,这样并不会"反馈"误差,也就没有累积放大的问题。
|
||||
|
||||
或者,根据精确重构出来的矩阵谱分解,得到的特征值作为'真实值',进行在线更新,执行单步计算。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
实时估计 为什么不用AI 能做预测 ,对于**完全随机网络**没有用 复杂度高 需要数据训练 算力时间
|
||||
|
||||
图神经
|
||||
|
||||
可以搞多维特征
|
||||
|
||||
AI对结构预测不准, 特征
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
376
项目/拼团交易系统.md
376
项目/拼团交易系统.md
@ -1,5 +1,25 @@
|
||||
# 拼团交易系统
|
||||
|
||||
## 系统启动说明
|
||||
|
||||
本系统涉及微信和支付宝的回调。
|
||||
|
||||
1.微信扫码登录,*https://mp.weixin.qq.com/debug/cgi-bin/sandboxinfo?action=showinfo&t=sandbox/index*平台上配置了扫描通知地址,如果是本地测试,需要打开frp内网穿透,然后填的地址是frp建立通道的服务器端的ip:端口
|
||||
|
||||
2.支付宝,用户付款成功回调,也是同理,本地测试就要开frp。注意frp中的通道,默认是本地端口=远程端口,但是如果在服务器上部署了一套,那么远程的端口就会与frp的端口冲突!!!导致本地测试的时候失效。
|
||||
|
||||
|
||||
|
||||
流程:
|
||||
|
||||
用户锁单-》支付宝付款-》成功后return_url设置了用户支付完毕后跳转回哪个地址是给前端用户看的; alipay_notify_url设置了支付成功后alipay调用你的后端哪个接口。
|
||||
|
||||
这里有小商城和拼团系统,notify_url指拼团系统中拼团达到指定人数后,通知小商城的地址,这里用rabbitmq。然后小商场将订单中相应拼团的status都设置为deal_done。然后小商场内部也再发一个'支付成功'消息,主要用于通知这些拼团对应的订单进入下一环节:发货(感觉'支付成功'取名不够直观)。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 系统设计
|
||||
|
||||
### **功能流程**
|
||||
@ -307,7 +327,52 @@ EndNode.apply() → 组装结果并返回 TrialBalanceEntity
|
||||
|
||||
|
||||
|
||||
**注意**两个回调函数不要搞混:1:alipay用户支付成功后的回调,告诉pay-mall当前用户支付完毕,可以调用拼团**组队结算**。2:group_buy_market的某teamId拼团达到目标人数,拼团成功的回调,告诉pay-mall可以进行下一步动作,比如给该teamId下的所有人发货。
|
||||
**注意**两个回调函数不要搞混:1:alipay_notify_url,用户支付成功后支付宝的回调,为后端服务设定的回调地址,支付宝告诉pay-mall当前用户支付完毕,可以调用拼团**组队结算**。return_rul,用户付款后自动跳转到的地址,即跳转回首页,和前端跳转有关。gateway-url,支付宝提供的本商家的用户付款地址。
|
||||
|
||||
2:group-buy-market_notify-url,由pay-mall商城设置的回调,某teamId拼团达到目标人数时,拼团成功会触发该回调,告诉pay-mall可以进行下一步动作,比如给该teamId下的所有人发货。
|
||||
|
||||
|
||||
|
||||
### 本地对接
|
||||
|
||||
在 `group-buying-sys` 项目中,对 `group-buying-api` 模块执行 `mvn clean install`(或在 IDE 中运行 install)。这会将该模块的 jar 安装到本地 Maven 仓库(`~/.m2/repository`)。然后在 `pay-mall` 项目的 `pom.xml` 中添加依赖,使用相同的 `groupId`、`artifactId` 和 `version` 即可引用该模块,如下所示:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>edu.whut</groupId>
|
||||
<artifactId>group-buying-api</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
|
||||
```
|
||||
|
||||
### 发包
|
||||
|
||||
仅适用于本地,共用一个本地Maven仓库,一旦换台电脑或者在云服务器上部署,无法就这样引入,因此可以进行发包。这里使用阿里云效发包https://packages.aliyun.com/
|
||||
|
||||
1)点击制品仓库->生产库
|
||||
|
||||

|
||||
|
||||
2)下载settings-aliyun.xml文件并保存至本地的Maven的conf文件夹中。
|
||||
|
||||

|
||||
|
||||
3) 配置项目的Maven仓库为阿里云提供的这个,而不是自己的本地仓库。
|
||||
|
||||

|
||||
|
||||
4)发包,打开Idea中的Maven,双击deploy
|
||||
|
||||

|
||||
|
||||
5)验证
|
||||
|
||||

|
||||
|
||||
6)使用
|
||||
|
||||
将公共镜像仓库的settings文件和阿里云效的settings文件合并,可以同时拉取公有依赖和私有包。
|
||||
|
||||
|
||||
|
||||
@ -871,6 +936,28 @@ public class SimpleAsyncDemo {
|
||||
|
||||
### 动态配置(热更新)
|
||||
|
||||
原理:借助 Redis 的发布/订阅(Pub/Sub)能力,在程序跑起来以后,动态地往某个频道推送一条消息,然后所有订阅了该频道的 Bean 都会收到通知,进而反射更新它们身上的对应字段。
|
||||
|
||||
```text
|
||||
启动时 ────────────────────────────────────▶ BeanPostProcessor
|
||||
│ 扫描 @DCCValue 写入默认 / 读取 Redis
|
||||
│ 注入字段值 缓存 key→Bean
|
||||
─────────────────────────────────────────────────────────────────
|
||||
运行时
|
||||
管理后台调用 ───▶ publish("myKey,newVal") ───▶ Redis Pub/Sub
|
||||
│ │
|
||||
│ ▼
|
||||
│ RTopic listener 收到消息
|
||||
│ └─ ▸ 写回 Redis
|
||||
│ └─ ▸ 从 Map 找到 Bean
|
||||
│ └─ ▸ 反射注入新值到字段
|
||||
▼
|
||||
Bean 字段热更新完成
|
||||
|
||||
```
|
||||
|
||||
#### 实现步骤
|
||||
|
||||
**注解标记**
|
||||
用 `@DCCValue("key:default")` 标注需要动态注入的字段,指定对应的 Redis Key(带前缀)及默认值。
|
||||
|
||||
@ -903,22 +990,38 @@ public class MyFeature {
|
||||
```java
|
||||
@Override
|
||||
public Object postProcessAfterInitialization(Object bean, String name) {
|
||||
// 确定真实的目标类:处理代理 Bean 或普通 Bean
|
||||
Class<?> cls = AopUtils.isAopProxy(bean)
|
||||
? AopUtils.getTargetClass(bean)
|
||||
: bean.getClass();
|
||||
? AopUtils.getTargetClass(bean)
|
||||
: bean.getClass();
|
||||
|
||||
// 遍历所有字段,寻找标注了 @DCCValue 的配置字段
|
||||
for (Field f : cls.getDeclaredFields()) {
|
||||
DCCValue dv = f.getAnnotation(DCCValue.class);
|
||||
if (dv==null) continue;
|
||||
String[] p = dv.value().split(":");
|
||||
String key = PREFIX + p[0], defaultValue = p[1];
|
||||
RBucket<String> bucket = redis.getBucket(key);
|
||||
String val = bucket.isExists() ? bucket.get() : defaultValue;
|
||||
bucket.trySet(defaultValue); //同步redis内容
|
||||
injectField(bean, f, val); //反射注入
|
||||
beans.put(key, bean);
|
||||
DCCValue dv = f.getAnnotation(DCCValue.class);
|
||||
if (dv == null) {
|
||||
continue; // 如果该字段未被 @DCCValue 注解标注,则跳过
|
||||
}
|
||||
|
||||
// 注解值格式为 "key:default",拆分获取配置项的 key 和默认值
|
||||
String[] parts = dv.value().split(":");
|
||||
String key = PREFIX + parts[0]; // Redis 中存储该配置的完整 Key
|
||||
String defaultValue = parts[1]; // 默认值
|
||||
|
||||
// 从 Redis 获取配置,如果不存在则使用默认值,并同步写入 Redis
|
||||
RBucket<String> bucket = redis.getBucket(key);
|
||||
String val = bucket.isExists() ? bucket.get() : defaultValue;
|
||||
bucket.trySet(defaultValue); // 如果 Redis 中没有该 Key,则写入默认值
|
||||
|
||||
// 反射方式将值注入到 Bean 的字段上(即动态替换该字段的值)
|
||||
injectField(bean, f, val);
|
||||
|
||||
// 将该 Bean 注册到映射表,以便后续热更新时找到实例并更新字段
|
||||
beans.put(key, bean);
|
||||
}
|
||||
return bean;
|
||||
}
|
||||
|
||||
return bean; // 返回处理后的 Bean
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**运行时热更新**
|
||||
@ -941,21 +1044,39 @@ public Object postProcessAfterInitialization(Object bean, String name) {
|
||||
2. 从映射里取出对应 Bean,反射更新它的字段。
|
||||
|
||||
```java
|
||||
// 发布/订阅频道
|
||||
@Bean
|
||||
public RTopic dccTopic() {
|
||||
// 发布/订阅频道,用于接收 DCC 配置的热更新消息
|
||||
@Bean
|
||||
public RTopic dccTopic() {
|
||||
// 1. 从 RedissonClient 中获取名为 "dcc_update" 的主题(Topic),后续会订阅这个频道
|
||||
RTopic t = redis.getTopic("dcc_update");
|
||||
t.addListener(String.class, (c,msg)->{
|
||||
String[] a = msg.split(",");
|
||||
String key = PREFIX + a[0], val = a[1];
|
||||
RBucket<String> bucket = redis.getBucket(key);
|
||||
if (!bucket.isExists()) return;
|
||||
bucket.set(val);
|
||||
Object bean = beans.get(key);
|
||||
if (bean!=null) injectField(bean, a[0], val);
|
||||
|
||||
// 2. 为该主题添加监听器,消息格式为 String
|
||||
t.addListener(String.class, (channel, msg) -> {
|
||||
// 3. msg 约定格式:"configKey,newValue",先按逗号分割出 key 和 value
|
||||
String[] a = msg.split(",");
|
||||
String key = PREFIX + a[0]; // 拼出完整的 Redis Key
|
||||
String val = a[1]; // 新的配置值
|
||||
|
||||
// 4. 检查 Redis 中是否已存在该 Key(只对已注册的配置生效)
|
||||
RBucket<String> bucket = redis.getBucket(key);
|
||||
if (!bucket.isExists()) {
|
||||
return; // 如果不是我们关心的配置,跳过
|
||||
}
|
||||
|
||||
// 5. 把新值同步写回 Redis,保证持久化
|
||||
bucket.set(val);
|
||||
|
||||
// 6. 从内存缓存中取出当初注入该 key 的 Bean 实例
|
||||
Object bean = beans.get(key);
|
||||
if (bean != null) {
|
||||
// 7. 通过反射把新的配置值重新注入到 Bean 的字段上,完成热更新
|
||||
injectField(bean, a[0], val);
|
||||
}
|
||||
});
|
||||
|
||||
// 8. 返回这个 RTopic Bean,让 Spring 容器管理
|
||||
return t;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@ -1300,3 +1421,206 @@ if (openid != null) {
|
||||
```
|
||||
|
||||
4)**成功后**,前端拿到 openid/JWT,直接进入应用,无需用户任何操作。
|
||||
|
||||
|
||||
|
||||
### 独占锁和无锁化场景
|
||||
|
||||
#### 独占锁
|
||||
|
||||
**适用场景**
|
||||
|
||||
- **定时任务互备**
|
||||
多机部署时,确保每天只有一台机器在某个时间点执行同一份任务(如数据清理、报表生成、邮件推送等)。
|
||||
|
||||
```java
|
||||
@Scheduled(cron = "0 0 0 * * ?")
|
||||
public void exec() {
|
||||
// 获取锁句柄,并未真正获取锁
|
||||
RLock lock = redissonClient.getLock("group_buy_market_notify_job_exec");
|
||||
try {
|
||||
//尝试获取锁 waitTime = 3:如果当前锁已经被别人持有,调用线程最多等待 3 秒去重试获取;leaseTime = 0:不设过期时间,看门狗机制
|
||||
boolean isLocked = lock.tryLock(3, 0, TimeUnit.SECONDS);
|
||||
if (!isLocked) return;
|
||||
Map<String, Integer> result = tradeSettlementOrderService.execSettlementNotifyJob();
|
||||
log.info("定时任务,回调通知拼团完结任务 result:{}", JSON.toJSONString(result));
|
||||
} catch (Exception e) {
|
||||
log.error("定时任务,回调通知拼团完结任务失败", e);
|
||||
} finally {
|
||||
if (lock.isLocked() && lock.isHeldByCurrentThread()) {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 无锁化场景
|
||||
|
||||
“无锁化”设计 的核心思路是不在整个逻辑上加一把全局互斥锁,而是用 **Redis 原子操作** + **后置校验/补偿** 来完成并发控制。
|
||||
|
||||
**原子计数(Atomic Counter)**
|
||||
用 Redis 的 `INCR`(或 Redisson 的 `RAtomicLong.incrementAndGet()`)来保证并发环境下每次调用都能拿到一个唯一、自增的数字。这个数字可以看作“第 N 个占位请求”。
|
||||
|
||||
**边界校验+补偿回滚(Validation & Compensation)**
|
||||
拿到新数字后,马上与允许的最大值(`target + 已回滚补偿数`)做比较:
|
||||
|
||||
- 如果在范围内,视为占位成功;
|
||||
- 如果超出范围,则把 Redis 里的计数器重置回 `target`(即“丢弃”这次多余的自增),并返回失败。
|
||||
|
||||
**极端兜底锁(Fallback Lock)**
|
||||
虽然 `INCR` 本身已经原子,但在极端运维或网络抖动下仍有极小几率两次自增同时返回相同值。
|
||||
因此,针对每个“序号”再做一次最轻量的 `SETNX(key:occupySeq)`:
|
||||
|
||||
- 成功 `SETNX` → 序号唯一,真正拿到名额;
|
||||
- 失败 `SETNX` → 重复抢号,拒绝这次占位。
|
||||
|
||||
**典型适用场景**
|
||||
|
||||
- **电商秒杀 & 拼团抢购**
|
||||
万级甚至十万级并发下不适合所有请求都排队,必须让绝大多数请求用原子计数并行处理。
|
||||
- **抢票系统**
|
||||
票务分配、座位预占,都讲究“先到先得”+“补偿回退”,不能用一把大锁。
|
||||
|
||||
```java
|
||||
@Override
|
||||
public boolean tryOccupy(String counterKey,
|
||||
String recoveryKey,
|
||||
int target,
|
||||
int ttlMinutes) {
|
||||
|
||||
// 1) 读取“补偿”次数(退款/回滚补偿)
|
||||
Long recovery = redisService.getAtomicLong(recoveryKey);
|
||||
int recovered = (recovery == null ? 0 : recovery.intValue());
|
||||
|
||||
// 2) 原子自增,拿到当前序号
|
||||
long seq = redisService.incr(counterKey);
|
||||
long occupySeq = seq;
|
||||
|
||||
// 3) 超出“目标 + 补偿池” → 回滚主计数器,失败
|
||||
if (occupySeq > target + recovered) {
|
||||
redisService.setAtomicLong(counterKey, target);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4) 如果用到了补偿名额(序号已经 > target),就从补偿池里减掉一个
|
||||
//if (occupySeq > target) {
|
||||
// redisService.decr(recoveryKey);
|
||||
//}
|
||||
|
||||
// 5) 兜底锁:针对每个序号做一次 SETNX,防止极端重复
|
||||
String lockKey = counterKey + ":lock:" + occupySeq;
|
||||
boolean locked = redisService.setNx(lockKey, ttlMinutes, TimeUnit.MINUTES);
|
||||
if (!locked) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 6) 成功占位
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
### `Supplier<T>`
|
||||
|
||||
`Supplier<T>` 是 Java 8 提供的一个函数式接口
|
||||
|
||||
```java
|
||||
@FunctionalInterface
|
||||
public interface Supplier<T> {
|
||||
/**
|
||||
* 返回一个 T 类型的结果,参数为空
|
||||
*/
|
||||
T get();
|
||||
}
|
||||
```
|
||||
|
||||
任何“无参返回一个 T 类型对象”的代码片段(方法引用或 lambda)都可以当成 `Supplier<T>` 来用。
|
||||
|
||||
#### **作用**
|
||||
|
||||
**1.延迟执行**
|
||||
把“取数据库数据”这类开销大的操作,包装成 `Supplier<T>` 传进去;只有真正需要时(缓存未命中),才调用 `supplier.get()` 去跑查询。
|
||||
|
||||
**2.解耦逻辑**
|
||||
缓存逻辑和查询逻辑分离,缓存组件不用知道“怎么查库”,只负责“啥时候要查”,调用方通过 `Supplier` 把查库方法交给它。
|
||||
|
||||
**3.重用性高**
|
||||
同一个缓存-回源模板方法可以服务于任何返回 `T` 的场景,既可以查 `User`,也可以查 `Order`、`List<Product>`……
|
||||
|
||||
```java
|
||||
// 服务方法:它只关心“缓存优先,否则回源”
|
||||
// dbFallback 是一段延迟执行的查库代码
|
||||
protected <T> T getFromCacheOrDb(String cacheKey, Supplier<T> dbFallback) {
|
||||
// 1) 先从缓存拿
|
||||
T v = cache.get(cacheKey);
|
||||
if (v != null) return v;
|
||||
|
||||
// 2) 缓存没命中,调用 dbFallback.get() 去“回源”拿数据
|
||||
T fromDb = dbFallback.get();
|
||||
if (fromDb != null) {
|
||||
cache.put(cacheKey, fromDb);
|
||||
}
|
||||
return fromDb;
|
||||
}
|
||||
|
||||
// 调用时这么写:
|
||||
User user = getFromCacheOrDb(
|
||||
"user:42",
|
||||
() -> userRepository.findById(42) // 这里的 () -> ... 就是一个 Supplier<User>
|
||||
);
|
||||
|
||||
List<Product> list = getFromCacheOrDb(
|
||||
"hot:products",
|
||||
() -> productService.queryHotProducts() // Supplier<List<Product>>
|
||||
);
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 动态限流+黑名单
|
||||
|
||||
<img src="https://pic.bitday.top/i/2025/07/24/rbw21x-0.jpg" alt="ce1092e98bdb7d396589a46376b872a4" style="zoom:67%;" />
|
||||
|
||||
**令牌桶算法(Token Bucket)**
|
||||
|
||||
- 按固定速率往桶里放“令牌”(tokens),比如每秒放 N 个;
|
||||
- 每次请求来临时“取一个令牌”才能通过,取不到就拒绝或降级;
|
||||
- 可以做到“流量平滑释放”、“突发流量吸纳”(桶里最多能积攒 M 个令牌)。
|
||||
|
||||
**核心限流思路**
|
||||
|
||||
- **注解驱动拦截**:对标记了 `@RateLimiterAccessInterceptor` 的方法统一进行限流。
|
||||
- **分布式限流**:基于 Redisson 的 `RRateLimiter`,可在多实例环境下共享令牌桶。
|
||||
- **黑名单机制**:对超限用户计数,达到阈值后加入黑名单(24 h 后自动解禁)。
|
||||
- **动态开关**:通过 DCC 配置中心开关(`rateLimiterSwitch`)可随时启用或关闭限流。
|
||||
- **降级回调**:限流或黑名单命中时,通过注解指定的方法反射调用,返回自定义响应。
|
||||
|
||||
```text
|
||||
请求到达
|
||||
↓
|
||||
检查限流开关(DCC)
|
||||
↓
|
||||
解析限流维度(key,如 userId)
|
||||
↓
|
||||
黑名单校验(RAtomicLong 计数,24h 过期)
|
||||
↓
|
||||
分布式令牌桶限流(RRateLimiter.tryAcquire)
|
||||
↓
|
||||
├─ 通过 → 执行目标方法
|
||||
└─ 拒绝 → 调用 fallback 方法,记录黑名单次数
|
||||
```
|
||||
|
||||
|
||||
|
||||
| 对比维度 | 本地限流 | 分布式限流 |
|
||||
| ----------------- | ---------------------------------------------------- | ------------------------------------------------------------ |
|
||||
| 实现复杂度 | **低**:直接用 Guava `RateLimiter`,几行代码即可接入 | **中高**:依赖 Redis/Redisson,需要注入客户端并管理限流器 |
|
||||
| 性能开销 | **极低**:全程内存操作,纳秒级延迟 | **中等**:每次获取令牌需网络往返,存在 RTT 延迟 |
|
||||
| 限流范围 | **单实例**:仅对当前 JVM 有效,多实例互不影响 | **全局**:多实例共享同一套令牌桶,合计速率可控 |
|
||||
| 状态持久化 & 容错 | **无**:服务重启后状态丢失;实例宕机只影响自身 | **有**:Redis 存储限流器与黑名单,可持久化;需保证 Redis 可用性 |
|
||||
| 监控 & 可观测 | **弱**:需额外上报或埋点才能集中监控 | **强**:可直接查看 Redis Key、TTL、计数等,易做报警与可视化 |
|
||||
| 运维依赖 | **无**:不依赖外部组件 | **有**:需维护高可用的 Redis 集群,增加运维成本 |
|
||||
|
||||
目前本项目使用的是分布式限流,用Redisson
|
||||
|
Loading…
x
Reference in New Issue
Block a user