Appearance
Kafka 重复消费治理案例:订单消息重复扣减库存时怎么做幂等
Kafka 很强,但它不是“天然不会重复”的系统。
真正到了线上,重复消费很常见,尤其体现在:
- 扣库存重复
- 发券重复
- 积分重复增加
场景
订单支付成功后发送一条扣减库存消息。
某次线上抖动后,库存服务出现了重复扣减,排查发现:
- 消费端业务执行成功了
- 但提交位点前进得不稳定
- 实例重启后同一条消息被再次消费
先说结论
Kafka 重复消费并不罕见,真正重要的是:
- 接受重复可能发生
- 在业务侧把幂等做实
更实用的思路通常是:
- 先搞清重复发生在哪个阶段
- 再明确幂等键是什么
- 最后把“去重记录”和“业务状态变更”绑到同一个事务边界里
一、为什么会重复消费
最常见的场景有:
- 业务处理成功,但还没来得及提交 offset
- 消费过程超时或抛异常,随后重试
- Rebalance 期间同一批消息被重新分配
- 消费端自己做了重试,但业务操作不是幂等的
所以要先接受一个事实:
- Kafka 更偏向可靠投递
- 不是天然只处理一次
二、不要把“幂等”理解得太抽象
幂等的核心问题其实很具体:
- 同一条业务事件重复执行,结果还能不能保持正确
比如扣库存:
- 正确结果应该是扣一次
如果一条消息来了两次,你的系统还扣了两次,那幂等就没做好。
三、幂等键到底怎么选
这一步非常关键。
比较常见的选择有:
- 订单号
- 订单号加事件类型
- 业务流水号
- 消息唯一 ID
真正选择时要优先看:
- 什么字段能稳定代表“这是同一件业务事”
如果只用 Kafka 分区位点做幂等键,通常不稳,因为它更像消息系统内部坐标,不一定适合业务语义。
四、为什么只查一次数据库还不够
很多实现会这么做:
- 先查这笔业务处理过没有
- 没处理过就执行
- 执行完再标记已处理
问题在于并发或重试下,这三步如果不在同一个事务边界里,就可能出现:
- 两个消费者都查到“没处理过”
- 然后都执行成功
所以更稳妥的方式通常是:
- 用唯一约束、状态机或去重表,把去重和业务变更绑定起来
五、一个更实用的落地思路
1. 建立业务去重键
让系统能明确判断:
- 这条消息代表的是不是已经处理过的业务事件
2. 去重记录和业务更新一起提交
比如:
- 插入去重表成功才继续执行业务
- 或利用业务表唯一约束保证重复事件不会二次生效
3. 失败时明确区分“可重试”和“不可重试”
不是所有异常都该无限重试。
如果是数据脏了、字段非法、业务状态不满足,再怎么重试也没意义。
4. 做好重试次数和死信兜底
这样才能避免重复消息把消费线程长期拖住。
六、一个典型复盘怎么理解
比如库存扣减重复的根因可能是:
- 消费业务执行成功
- 数据库也提交成功
- 但提交 offset 前实例重启
实例恢复后,这条消息再次被拉取,结果再次扣库存。
如果库存扣减逻辑没有按订单号做幂等,问题就会真正落地成数据错误。
七、最容易踩的坑
1. 以为开启幂等生产者就万事大吉
幂等生产者主要解决的是:
- 生产端重复写入风险
它不等于消费端业务天然幂等。
2. 把幂等做到缓存里
缓存适合做加速,不适合做最终一致的幂等判断。
关键去重信息最好还是落到更稳定的存储里。
3. 无限制重试
如果业务本身不幂等,无限制重试只会把错误放大。
一句话总结
Kafka 重复消费不是异常,而是需要被正面设计进去的常态。
真正关键的不是“怎么保证绝不重复”,而是:
- 就算重复来了
- 业务结果也依然正确