结论
先抛结论:保证不了
为什么会重复消费?
根本原因:ACK 机制
Consumer 消费成功了
但是 ACK 回执网络丢包
Broker 以为没消费
重新投递 → 重复消费
RocketMQ 只能保证消息「至少消费一次」(At Least Once)
不能保证「仅消费一次」
解决方案
唯一解法:消费端做幂等!
同一个消息,消费 1 次和 100 次,结果一样。
3 种最实用的幂等方案
方案 1:唯一消息 ID + 去重表(最常用、最简单)
流程:
每条消息有 唯一业务 ID(订单号、支付流水号)
消费时:
先查 DB 是否存在该 ID
存在 → 直接返回,不处理
不存在 → 执行业务,插入去重表
事务控制:业务逻辑 + 插入去重表 同一个事务
方案 2:利用数据库唯一约束(最简单)
给业务表的 业务 ID 加唯一索引
重复消息插入时
数据库直接报唯一键冲突
捕获异常,直接返回
方案 3:Redis 分布式锁(高并发)
消息 ID 作为 Redis Key
消费前:
SETNX lock:orderId
加锁成功 → 消费
加锁失败 → 重复
其实上面的方案都有问题
方案 1:唯一 ID + 去重表
高并发下绝对有风险!
数据库压力爆炸
每一条消息都要 select + insert,并发 1w+ 直接把库打垮
并发重复
两个相同请求同时 select → 都没查到 → 都执行 → 依旧重复
去重表无限膨胀
数据越来越大,查询越来越慢
方案 2:数据库唯一约束
高并发下大量异常报错
只能用于插入,不能用于更新(比如扣库存)
无法处理复杂业务
方案 3:Redis 分布式锁
锁超时风险
主从切换锁丢失
重入问题复杂
真正生成用法
不靠查询、不靠锁、不靠去重表,
靠【业务唯一 ID + 状态机 + 原子 UPDATE】!**
举2个例子
1.订单状态修改
修改状态无论多少次都是一样的结果
java
UPDATE order
SET status = '已取消'
WHERE orderId = #{orderId}
AND status = '待取消'
2.库存扣减
库存没办法像订单表一样修改状态,只能用 去重表+扣减 来幂等
java
@Transactional
public void consume(Message message) {
String orderId = message.getUserProperty("orderId");
Long goodsId = Long.valueOf(message.getUserProperty("goodsId"));
// ==========================
// 1. 幂等去重(唯一键)
// ==========================
try {
stockUniqueLogMapper.insert(orderId);
} catch (DuplicateKeyException e) {
log.info("重复消费,直接ACK: {}", orderId);
return;
}
// ==========================
// 2. 扣库存
// ==========================
int rows = stockMapper.deductStock(goodsId);
// ==========================
// 3. 扣减成功 → ACK
// ==========================
if (rows > 0) {
log.info("扣减成功,ACK: {}", orderId);
return;
}
// ==========================
// 4. 库存不足 → ACK + 补偿
// ==========================
log.error("库存不足,业务失败,ACK,发送补偿: {}", orderId);
sendRefundMessage(orderId);
}