Commit on 2025/07/24 周四 17:54:56.38

This commit is contained in:
zhangsan 2025-07-24 17:54:56 +08:00
parent 59de4f24a6
commit 3f0c608c84
8 changed files with 826 additions and 37 deletions

View File

@ -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()

View File

@ -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("库存不足");
}
}
}
```

View File

@ -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) {

View File

@ -128,7 +128,7 @@ public class SpringAmqpTest {
}
```
`convertAndSend`如果 2 个参数,第一个表示队列名,第二个表示消息;
**消息接收**
@ -151,6 +151,8 @@ public class SpringRabbitListener {
### 交换机
无论是 **Direct**、**Topic** 还是 **Fanout** 交换机,你都可以用 **同一个 Binding Key** 把多条队列绑定到**同一个交换机**上。
**1fanout广播给每个绑定的队列**
![image-20250528133703660](https://pic.bitday.top/i/2025/05/28/m40swr-0.png)
@ -159,7 +161,7 @@ public class SpringRabbitListener {
发送消息:
`convertAndSend`如果2个参数第一个表示队列名第二个表示消息如果3个参数,第一个表示交换机,第二个表示`RoutingKey`,第三个表示消息。
`convertAndSend`如果 3 个参数,第一个表示交换机,第二个表示`RoutingKey`,第三个表示消息。
```java
@Test
@ -188,15 +190,15 @@ public void testFanoutExchange() {
**3Topic交换机**
`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 管理**控制台手动创建**队列和交换机,开发人员还得把所有配置整理一遍交给运维,既繁琐又容易出错。更好的做法是在应用启动时自动检测所需的队列和交换机,若不存在则直接创建。
**基于注解方式来声明**

View File

@ -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

View File

@ -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:

View File

@ -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对结构预测不准 特征

View File

@ -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
**注意**两个回调函数不要搞混1alipay用户支付成功后的回调告诉pay-mall当前用户支付完毕可以调用拼团**组队结算**。2group_buy_market的某teamId拼团达到目标人数拼团成功的回调告诉pay-mall可以进行下一步动作比如给该teamId下的所有人发货。
**注意**两个回调函数不要搞混1alipay_notify_url用户支付成功后支付宝的回调为后端服务设定的回调地址支付宝告诉pay-mall当前用户支付完毕可以调用拼团**组队结算**。return_rul用户付款后自动跳转到的地址即跳转回首页和前端跳转有关。gateway-url支付宝提供的本商家的用户付款地址。
2group-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点击制品仓库->生产库
![image-20250724141043193](https://pic.bitday.top/i/2025/07/24/nbnrr4-0.png)
2下载settings-aliyun.xml文件并保存至本地的Maven的conf文件夹中。
![image-20250724141436208](https://pic.bitday.top/i/2025/07/24/ndyg97-0.png)
3 配置项目的Maven仓库为阿里云提供的这个而不是自己的本地仓库。
![image-20250724141557398](https://pic.bitday.top/i/2025/07/24/neod28-0.png)
4发包打开Idea中的Maven双击deploy
![image-20250724141700856](https://pic.bitday.top/i/2025/07/24/nfar4l-0.png)
5验证
![image-20250724141758595](https://pic.bitday.top/i/2025/07/24/nfvnao-0.png)
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