Skip to content

Commit cf38afa

Browse files
committed
update blog
1 parent 7e06f84 commit cf38afa

2 files changed

Lines changed: 276 additions & 0 deletions

File tree

src/content/posts/20250703.md

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
---
2+
title: MQ消息重复消费的原因、解决方案对比
3+
description: 消息重复消费是很多系统都会碰到的麻烦事,本文先讲清楚为什么会重复,然后说说用 Redis 和消息确认(ACK)能不能解决问题,哪里还会出坑。最后给你介绍几个靠谱的解决方案,帮你保证消息不丢也不重复,让业务更稳更安全。
4+
published: 2025-07-03
5+
tags: [MQ, java, 后端, spring boot]
6+
category: 原创
7+
draft: false
8+
---
9+
10+
# 1. 介绍消息重复消费产生的原因
11+
12+
生产端为了保证消息发送成功,可能会重复推送(直到收到成功ACK),会产生重复消息。
13+
一个成熟的MQ Server框架一般会想办法解决,避免存储重复消息(比如:空间换时间,存储已处理过的message\_id),给生产端提供一个幂等性的发送消息接口。
14+
消费端却无法根本解决这个问题,在高并发标准要求下,拉取消息+业务处理+提交消费位移需要做事务处理,另外消费端服务可能宕机,很可能会拉取到重复消息。
15+
所以,只能业务端自己做控制,对于已经消费成功的消息,本地数据库表或Redis缓存业务标识,每次处理前先进行校验,保证幂等。
16+
17+
18+
# 2. 消费端为什么会有消息重复消费问题
19+
20+
ACK 是在「消息处理完成后」才返回的
21+
这就导致了:
22+
如果消费过程中挂了、超时了、异常了…
23+
24+
* RabbitMQ 没有收到 ACK
25+
* 它会认为这条消息「还没有被成功消费」
26+
* 于是再次投递(可能给同一个消费者,也可能是另一个)
27+
28+
所以问题来了:
29+
在消息“还没 ACK”之前,你的业务逻辑可能已经执行了
30+
比如:
31+
32+
```java
33+
// 假设你在消费方法里:
34+
createOrder(orderNo); // 执行成功 ✅
35+
channel.basicAck(tag, false); // 正准备 ack,此时服务崩溃 ❌
36+
```
37+
38+
RabbitMQ:哦,我没收到 ack,你没消费成功,我再发一次 ✅
39+
40+
结果:
41+
42+
* createOrder 又执行一次 (重复下单、重复扣款、重复发优惠券)
43+
44+
# 3. 初步解决消息重复消费问题
45+
46+
自己加幂等判断:Redis SETNX 或数据库唯一约束
47+
48+
```java
49+
Boolean ok = redis.setIfAbsent("order:created:" + orderNo, "1", 1, TimeUnit.DAYS);
50+
if (!ok) {
51+
// 已处理过,直接 ack 跳过逻辑
52+
channel.basicAck(tag, false);
53+
return;
54+
}
55+
createOrder(orderNo); // 只执行一次
56+
channel.basicAck(tag, false); // 最后才 ack
57+
```
58+
59+
正确的消费端顺序是:
60+
61+
1. 消息进入监听器,提取 messageId 或业务 key
62+
2. 用 Redis/DB 检查这个 message 是否处理过(幂等性)
63+
3. 没处理过 ➜ 执行业务逻辑 ➜ 成功后 ack
64+
4. 如果处理过 ➜ 直接 ack,跳过逻辑
65+
66+
**小结:ACK vs SETNX**
67+
68+
| 场景 | 作用 |
69+
| -------------- | -------------------- |
70+
| ACK | 确保 MQ 不重复推消息(避免重复投递) |
71+
| Redis SETNX/DB | 确保业务逻辑不重复处理(避免重复执行) |
72+
73+
74+
# 4. Redis SETNX + MQ ACK,看似双保险,但实际上仍然存在两种风险:
75+
76+
**情况一:SETNX 后宕机,业务未执行**
77+
78+
* Redis 中已记录“已处理”状态;
79+
* 但业务未真正执行(如扣款没发生);
80+
* 后续 RabbitMQ 重发消息,消费端因为 Redis 中已标记,跳过处理;
81+
* ❗导致消息丢失,业务永远不会执行
82+
83+
**情况二:业务执行成功,但 ACK 之前宕机**
84+
85+
* 业务逻辑已经执行成功(扣款/下单等);
86+
* 但尚未发送 basicAck;
87+
* RabbitMQ 以为失败,重发消息;
88+
* 消费端再次处理,因为 Redis 中没有记录,还会重复执行业务逻辑;
89+
* ❗导致业务重复执行,数据异常
90+
91+
使用 Redis SETNX + ACK 并不能完全规避消息重复消费和消息丢失的问题。它解决了一部分问题,但中间过程依然存在“非原子性风险”。
92+
93+
**根本原因:三步操作不是原子事务**
94+
95+
1. SETNX
96+
2. 执行业务逻辑
97+
3. ACK
98+
99+
任意一个步骤失败,都会产生“重复 or 丢失”的风险。
100+
101+
102+
# 5. 理想解决方案
103+
104+
理想解决方式是:
105+
使用 “原子事务” 绑定 幂等记录、业务逻辑 和 ACK,一起成功或失败
106+
107+
### 方法一:使用数据库事务绑定幂等记录 + 业务逻辑 + ACK
108+
109+
```java
110+
@Transactional
111+
public void consumeMessage(...) {
112+
if (messageExistsInDb(messageId)) {
113+
ack(); return;
114+
}
115+
116+
doBusiness(); // 下订单、扣款等
117+
saveMessageRecord(messageId); // 记录已处理
118+
ack(); // 只在事务最后才发送 ack
119+
}
120+
```
121+
122+
* ✔️ 优点:所有动作在一个本地事务中提交或失败
123+
* ✔️ 缺点:需要所有动作都走数据库,牺牲了 Redis 的性能优势
124+
125+
126+
### 方法二:使用 Redis + 状态标记 + ACK 的最终一致性方案(推荐)
127+
128+
```java
129+
String key = "msg:" + messageId;
130+
131+
// 设置状态为处理中
132+
Boolean first = redis.setIfAbsent(key, "PROCESSING", 10, TimeUnit.MINUTES);
133+
if (!first) {
134+
// 如果状态是 DONE 或正在处理,说明已处理或处理中,跳过
135+
return;
136+
}
137+
138+
try {
139+
doBusiness(); // 真正的业务逻辑
140+
redis.set(key, "DONE", 1, TimeUnit.DAYS); // 标记业务完成
141+
channel.basicAck(tag, false); // ack 最后再发
142+
} catch (Exception e) {
143+
redis.delete(key); // 处理失败清除 key,允许下次重试
144+
channel.basicNack(tag, false, true); // 消息重回队列
145+
}
146+
```
147+
148+
* ✔️ 优点:兼顾幂等性与可靠性
149+
* ✔️ 避免业务未执行就标记“已处理”
150+
* ✔️ ACK 始终在最后,确保业务成功才确认
151+
152+
### 方法三(高级):使用 RocketMQ/Kafka 的事务消息或 exactly-once 语义(RabbitMQ 不支持)

src/content/posts/20250720.md

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
---
2+
title: 大型互联网项目架构及技术选型
3+
description: 介绍了SOA与微服务架构的核心理念及区别,分析了SpringCloud与Dubbo两大服务框架的定位与生态对比,阐述了RPC与Feign在服务间通信中的应用场景和优劣,最后对分布式锁的主流实现Zookeeper与Redis进行了对比,帮助读者全面理解互联网项目架构设计与技术选型的关键要点。
4+
published: 2025-07-20
5+
tags: [架构, dubbo, rpc, spring cloud, 微服务]
6+
category: 原创
7+
draft: false
8+
---
9+
10+
## 一、大型互联网项目架构目标
11+
12+
- **高性能**:提供快速的访问体验。
13+
- **高可用**:网站服务一直可以正常访问。
14+
- **可伸缩**:通过硬件增加/减少,提高/降低处理能力。
15+
- **高可扩展**:系统间耦合低,方便的通过新增/移除方式,增加/减少新的功能/模块。
16+
17+
18+
## 二、SOA架构和微服务架构
19+
20+
### 1. SOA架构(面向服务架构)
21+
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/babaf9902a424e24bc5dcc2fe3a7b8d4.png#pic_center)
22+
23+
SOA:(Service-Oriented Architecture,面向服务的架构)是一个组件模型,它将应用程序的不同功能单元(称为服务)进行拆分,并通过这些服务之间定义良好的接口和契约联系起来。
24+
25+
ESB:(Enterparise Servce Bus) 企业服务总线,服务中介。主要是提供了一个服务于服务之间的交互。ESB 包含的功能如:负载均衡,流量控制,加密处理,服务的监控,异常处理,监控告急等等。
26+
27+
### 2. 微服务架构
28+
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/ae3960d3401f49fbb3625222f6cdc14c.png#pic_center)
29+
30+
微服务架构是在 SOA 上做的升华,微服务架构强调的一个重点是“业务需要彻底的组件化和服务化”,原有的单个业务系统会拆分为多个可以独立开发、设计、运行的小应用。这些小应用之间通过服务完成交互和集成。
31+
32+
微服务架构 = 80%的SOA服务架构思想 + 100%的组件化架构思想 + 80%的领域建模思想。
33+
34+
**特点:**
35+
- 服务实现组件化:开发者可以自由选择开发技术。也不需要协调其他团队。
36+
- 服务之间交互一般使用REST API。
37+
- 去中心化:每个微服务有自己私有的数据库持久化业务数据。
38+
- 自动化部署:把应用拆分成为一个一个独立的单个服务,方便自动化部署、测试、运维。
39+
40+
41+
## 三、服务框架 - SpringCloud 和 Dubbo
42+
43+
### 1. 两者的区别
44+
45+
- 初始定位不同:SpringCloud定位为微服务架构下的一站式解决方案;Dubbo 是 SOA 时代的产物,它的关注点主要在于服务的调用和治理
46+
- 生态环境不同:SpringCloud依托于Spring平台,具备更加完善的生态体系;而Dubbo一开始只是做RPC远程调用,生态相对匮乏,现在逐渐丰富起来。
47+
- 调用方式:SpringCloud是采用Http协议做远程调用,接口一般是Rest风格,比较灵活;Dubbo是采用Dubbo协议,接口一般是Java的Service接口,格式固定。但调用时采用Netty的NIO方式,性能较好。
48+
- 组件差异比较多,例如SpringCloud注册中心一般用Eureka,而Dubbo用的是Zookeeper
49+
50+
### 2. 两者生态对比
51+
52+
| 功能 | Dubbo | SpringCloud |
53+
| -------------- | --------------- | ------------------------------------- |
54+
| 服务注册中心 | Zookeeper | Eureka(主流)、Consul、Zookeeper |
55+
| 服务调用方式 | RPC 基于 Dubbo协议 | REST API 基于 HTTP协议 |
56+
| 服务监控 | Dubbo-Monitor | Spring Boot Admin |
57+
| 熔断器 | 不完善 | Spring Cloud Netflix Hystrix |
58+
| 服务网关 || Spring Cloud Netflix Zuul、Gateway |
59+
| 分布式配置 || Spring Cloud Config |
60+
| 服务跟踪 || Spring Cloud Sleuth + Zipkin(常用) |
61+
| 数据流 || Spring Cloud Stream |
62+
| 批量任务 || Spring Cloud Task |
63+
| 信息总线 || Spring Cloud Bus |
64+
65+
Spring Cloud 的功能很明显比 Dubbo 更加强大,涵盖面更广,而且作为 Spring 的旗舰项目,它也能够与 Spring Framework、Spring Boot、Spring Data、Spring Batch 等其他 Spring 项目完美融合,这些对于微服务而言是至关重要的。
66+
67+
使用 Dubbo 构建的微服务架构就像组装电脑,各环节选择自由度很高,但是最终结果很有可能因为一条内存质量不行就点不亮了,总是让人不怎么放心,但是如果使用者是一名高手,那这些都不是问题。
68+
69+
70+
## 四、服务间通信 - RPC 和 Feign
71+
72+
### 1. 什么是RPC
73+
74+
RPC(Remote Procedure Call Protocol)远程过程调用协议。
75+
76+
一个通俗的描述是:客户端在不知道调用细节的情况下,调用存在于远程计算机上的某个对象,就像调用本地应用程序中的对象一样。比较正式的描述是:一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。
77+
78+
RPC是协议:既然是协议就只是一套规范,那么就需要有人遵循这套规范来进行实现。目前典型的RPC实现包括:Dubbo、Thrift、GRPC、Hetty等。
79+
80+
### 2. 什么是Feign
81+
82+
Feign是一个轻量级的Java库,专为简化HTTP API客户端调用而生。
83+
84+
通过动态代理和接口注解,Feign使得调用远程服务如同调用本地方法一样直观便捷。(可以认为Feign是RPC的一种)
85+
86+
它常用于Spring Cloud等微服务框架中,与服务发现、负载均衡等服务治理组件紧密集成,提供了一种声明式的HTTP客户端解决方案。
87+
88+
### 3. 两者对比
89+
90+
| 维度 | Feign | RPC(gRPC为例) |
91+
| ------------ | --------------- | --------------------------------- |
92+
| 协议 | 基于 HTTP/REST | 基于 HTTP/2 + 二进制协议 |
93+
| 性能 | 性能一般,延迟高 | 高性能,低延迟 |
94+
| 序列化 | JSON,简单但数据大 | Protobuf,效率高 |
95+
| 开发体验 | 注解 + 接口,易用 | 需 IDL,学习成本高 |
96+
| 跨语言支持 | Java 生态为主 | 多语言支持 |
97+
| 适用场景 | REST 接口、Web调用 | 内部高性能通信、跨语言调用 |
98+
99+
100+
## 五、分布式锁 - Zookeeper 和 Redis
101+
102+
### 1. Zookeeper 概念
103+
104+
Zookeeper 是 Apache Hadoop 项目下的一个子项目,是一个树形目录服务。Zookeeper 翻译过来就是 动物园管理员,他是用来管 Hadoop(大象)、Hive(蜜蜂)、Pig(小猪)的管理员。简称zk
105+
106+
Zookeeper 是一个分布式的、开源的分布式应用程序的协调服务。
107+
108+
Zookeeper 提供的主要功能包括:
109+
- 配置管理
110+
- 分布式锁
111+
- 集群管理
112+
113+
### 2. 两者对比
114+
115+
| 维度 | Zookeeper | Redis |
116+
| ------------ | ------------------- | ---------------------------------- |
117+
| 一致性 | 强一致性,顺序性好 | 最终一致性,依赖超时机制 |
118+
| 性能 | 较低,持久化操作较多 | 高,基于内存操作快 |
119+
| 可用性 | 高,节点崩溃自动释放锁 | 需哨兵或集群保障可靠性 |
120+
| 适用场景 | 可靠性要求高的分布式协调 | 高并发场景下的快速加锁 |
121+
122+
Zookeeper 更可靠,适合严格一致性场景;Redis 性能优越,适合高并发环境。
123+
124+
现在很少使用 Zookeeper,主要原因是许多组件已经不再依赖它。例如,Kafka 已经不用了,RocketMQ 自带了 nameserver,注册中心也有了 Nacos。这样可以减少一个中间件,降低运维和维护成本。

0 commit comments

Comments
 (0)