diff --git a/后端学习/Java笔记本.md b/后端学习/Java笔记本.md index b4dec53..cd6dc85 100644 --- a/后端学习/Java笔记本.md +++ b/后端学习/Java笔记本.md @@ -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 { + 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 stringBox = new Box<>(); + stringBox.set("Hello Generics"); + String s = stringBox.get(); // 自动类型推断为 String + System.out.println(s); + + // 创建一个只装 Integer 的盒子 + Box intBox = new Box<>(123); + Integer i = intBox.get(); + System.out.println(i); + } +} +``` + + + +### 定义一个泛型方法 + +有时候我们只想让某个方法支持多种类型,而不必为此写泛型类,就可以在方法前加上类型声明: + +```java +public class Utils { + //[修饰符] 返回类型 方法名(参数列表) { … } + // 泛型方法:打印任意类型的一维数组 + public static void printArray(T[] array) { + for (T element : array) { + System.out.println(element); + } + } +} +``` + +方法签名中 `` 表示这是一个泛型方法,`T` 在参数列表或返回值中使用。 + +调用时,编译器会根据传入实参**自动推断** `T`。 + +**使用** + +```java +public class Main { + public static void main(String[] args) { + String[] names = {"Alice", "Bob", "Charlie"}; + Utils.printArray(names); + // 等价于 Utils.printArray(names); + + Integer[] nums = {10, 20, 30}; + Utils.printArray(nums); + // 等价于 Utils.printArray(nums); + } +} +``` + + + ## 好用的方法 ### toString() diff --git a/后端学习/Redis.md b/后端学习/Redis.md index 728f9c0..1fff4f4 100644 --- a/后端学习/Redis.md +++ b/后端学习/Redis.md @@ -533,3 +533,111 @@ public void testCommon(){ +## Redisson + +### 快速入门 + +#### 1. 引入依赖 +```xml + + org.redisson + redisson-spring-boot-starter + 3.26.0 + +``` + +#### 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 buy(@PathVariable String productId) { + boolean ok = inventoryService.purchase(productId); + if (ok) { + return ResponseEntity.ok("购买成功"); + } else { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("库存不足"); + } + } +} +``` + diff --git a/后端学习/微服务.md b/后端学习/微服务.md index abd2256..5ca43c0 100644 --- a/后端学习/微服务.md +++ b/后端学习/微服务.md @@ -1457,7 +1457,7 @@ feign: image-20250526200028905 -``` +```java public class ItemClientFallback implements FallbackFactory { @Override public ItemClient create(Throwable cause) { diff --git a/后端学习/消息队列MQ.md b/后端学习/消息队列MQ.md index 4a6c0f3..ff4a9cf 100644 --- a/后端学习/消息队列MQ.md +++ b/后端学习/消息队列MQ.md @@ -128,7 +128,7 @@ public class SpringAmqpTest { } ``` - +`convertAndSend`如果 2 个参数,第一个表示队列名,第二个表示消息; **消息接收** @@ -151,6 +151,8 @@ public class SpringRabbitListener { ### 交换机 +无论是 **Direct**、**Topic** 还是 **Fanout** 交换机,你都可以用 **同一个 Binding Key** 把多条队列绑定到**同一个交换机**上。 + **1)fanout:广播给每个绑定的队列** ![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() { **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 管理**控制台手动创建**队列和交换机,开发人员还得把所有配置整理一遍交给运维,既繁琐又容易出错。更好的做法是在应用启动时自动检测所需的队列和交换机,若不存在则直接创建。 **基于注解方式来声明** diff --git a/杂项/Docker指南.md b/杂项/Docker指南.md index 13a27c3..e7dc15a 100644 --- a/杂项/Docker指南.md +++ b/杂项/Docker指南.md @@ -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 diff --git a/杂项/linux服务器.md b/杂项/linux服务器.md index 8e3b4cc..38b3b14 100644 --- a/杂项/linux服务器.md +++ b/杂项/linux服务器.md @@ -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: diff --git a/科研/小论文.md b/科研/小论文.md index cd65a0e..7c7df44 100644 --- a/科研/小论文.md +++ b/科研/小论文.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对结构预测不准, 特征 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/项目/拼团交易系统.md b/项目/拼团交易系统.md index c81a7d7..9ea7404 100644 --- a/项目/拼团交易系统.md +++ b/项目/拼团交易系统.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 + + edu.whut + group-buying-api + 1.0.0-SNAPSHOT + + +``` + +### 发包 + +仅适用于本地,共用一个本地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 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 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 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 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 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` + +`Supplier` 是 Java 8 提供的一个函数式接口 + +```java +@FunctionalInterface +public interface Supplier { + /** + * 返回一个 T 类型的结果,参数为空 + */ + T get(); +} +``` + +任何“无参返回一个 T 类型对象”的代码片段(方法引用或 lambda)都可以当成 `Supplier` 来用。 + +#### **作用** + +**1.延迟执行** +把“取数据库数据”这类开销大的操作,包装成 `Supplier` 传进去;只有真正需要时(缓存未命中),才调用 `supplier.get()` 去跑查询。 + +**2.解耦逻辑** +缓存逻辑和查询逻辑分离,缓存组件不用知道“怎么查库”,只负责“啥时候要查”,调用方通过 `Supplier` 把查库方法交给它。 + +**3.重用性高** +同一个缓存-回源模板方法可以服务于任何返回 `T` 的场景,既可以查 `User`,也可以查 `Order`、`List`…… + +```java +// 服务方法:它只关心“缓存优先,否则回源” +// dbFallback 是一段延迟执行的查库代码 +protected T getFromCacheOrDb(String cacheKey, Supplier 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 +); + +List list = getFromCacheOrDb( + "hot:products", + () -> productService.queryHotProducts() // Supplier> +); + +``` + + + +### 动态限流+黑名单 + +ce1092e98bdb7d396589a46376b872a4 + +**令牌桶算法(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