news 2026/10/20 11:12:26

消息队列的重复消费与顺序

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
消息队列的重复消费与顺序

消息队列的重复消费与顺序

订单消息明明只发了一条,订单却被创建了两次。日志里 MQ 没报错,生产端也没重发代码,重复是哪来的?另一个案子,消息按顺序发的,消费端却先处理了退款再处理支付。这两个问题是消息队列最经典的两道坎,答案都藏在「保证」这两个字的现实里。

重复从哪来

消息队列默认给你的是 at-least-once,至少投递一次。它宁可多发,不能漏发,重复是这个保证自带的价格。具体有三个来源。
生产端重试。网络抖动,生产者发完没收到确认,再发一次,broker 里就有了两条。
消费端没提交就挂。消费者处理完业务,还没来得及提交位点,进程挂了。重启后从旧位点再来一遍,同一条消息被处理两次。

broker 复制与重投。broker 主从切换、消费组重平衡,都可能把已投递未确认的消息再投一次。

想只消费一次怎么办?at-most-once,投了就不管,会丢消息,业务不敢用。exactly-once 看着美好,记住一句话:exactly-once 的真相是 at-least-once 加幂等。队列负责不漏,你的消费端负责处理两次结果一样。

幂等的三板斧

第一板斧,去重表。每条消息带唯一 ID,消费前查一下,处理过就跳过。去重表要和业务在同一个事务里,不然「查了没处理」和「处理了没记」都会破功。
第二板斧,业务自带幂等。数据库唯一约束(重复插入自然失败)、状态机流转(已支付的订单再收到支付消息,状态不允许流转)、乐观锁版本号(重复更新撞版本失败)。业务能自带幂等,去重表都可以省。

第三板斧,本地消息表。生产端把「发消息」和「改业务」放进同一个本地事务,事务提交后由后台任务补发消息,崩了就重发,配合消费端的幂等,端到端不丢不重。

1 业务 + 消息落库
同事务

2 补发(可能重发)

3 投递(可能重投)

4 去重或业务幂等

生产端

本地消息表

消息队列
at-least-once

消费端幂等

业务库

顺序难在哪

先摆一个事实,全局有序做得到,代价是整条队列只许一个分区一个消费者,吞吐回到单机。生产环境真用的是分区有序,同一个业务键(比如订单 id)的消息路由到同一个分区,分区内天然有序,消费端按序处理。

乱序的来源都在消费端。多线程消费,先拿到的后处理完。批量消费,一批里的消息被并行处理。重试插队,失败的消息重试时排到了后面。这些都会让「创建、支付、退款」的顺序错乱,状态机当场报错。

所以规矩是两条。生产端按业务键哈希选分区,同一个订单的消息进同一个分区。消费端要么单线程处理,要么按业务键分组串行。原则就一条:重复交给消费端,顺序交给分区。

亲手看两个现场

下面用一段 Python 复现重复投递和乱序的破坏力。直接运行python mq_demo.py,看幂等表怎么把两次执行拦成一次,乱序怎么让状态机拒绝。

# 消息重复与顺序的现场。直接运行:python mq_demo.pyimportrandomimportthreadingimporttimeclassConsumer:def__init__(self,idempotent=True):self.processed={}# 幂等表:消息 id -> 结果self.count=0# 业务真正执行的次数self.idempotent=idempotentdefhandle(self,msg_id,payload):ifself.idempotentandmsg_idinself.processed:print(f" [{msg_id}] 重复投递,幂等表拦下")returnself.count+=1self.processed[msg_id]=payloadprint(f" [{msg_id}] 真正执行:{payload}")defredeliver(consumer,msg):"""模拟处理完没来得及 ack 就挂,broker 重投同一条。"""msg_id,payload=msg consumer.handle(msg_id,payload)print(f" [{msg_id}] 没来得及 ack 就挂了,broker 重投")consumer.handle(msg_id,payload)print("== 重复现场 ==")raw=Consumer(idempotent=False)redeliver(raw,("m-1","扣库存 1 个"))print(f"裸消费:业务执行{raw.count}次,库存被扣了两次")safe=Consumer(idempotent=True)redeliver(safe,("m-1","扣库存 1 个"))print(f"幂等消费:业务执行{safe.count}次")defapply_action(state,action):"""订单状态机:创建 -> 支付 -> 退款,乱序直接拒绝。"""rules={("init","创建"):"created",("created","支付"):"paid",("paid","退款"):"refunded",}nxt=rules.get((state,action))returnnxtifnxtelsef"拒绝({state}->{action})"print("\n== 顺序现场 ==")flow=["创建","支付","退款"]state="init"print("分区有序:",end=" ")forainflow:state=apply_action(state,a)print(f"{a}->{state}",end=" ")print()shuffled=flow[:]random.shuffle(shuffled)state="init"print("并发乱序:",end=" ")forainshuffled:state=apply_action(state,a)print(f"{a}->{state}",end=" ")print()print("结论:顺序靠分区,重复靠幂等。")

适用边界

这一篇管重复与顺序两件事。消息积压、事务消息的实现细节(半消息、回查)、以及 Kafka 幂等生产者和事务 API 的机制,是另一个题。去重表的键设计随业务变,一般是业务单据号而不是消息 id,因为同一个业务动作可能产生多条消息。跑完演示对照你线上的消费端,先问一句,同一条消息进来两次,我的代码结果一样吗。答案是「不确定」的地方,就是下一个要修的 bug。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 11:10:40

学生CV代码短期部署:腾讯云轻量服务器快速验证指南

简介:本资源是一份面向高校计算机视觉方向学生的实战部署指南,聚焦疫情期间无法返校使用本地服务器时的低成本替代方案,详细讲解如何利用腾讯云新用户短期免费权益(15天)快速部署并运行Python 3.7 PyTorch CPU版代码。…

作者头像 李华
网站建设 2026/10/20 11:10:17

从JS数组到手写ArrayList:扩容机制与刷题字符串处理技巧

1. 为什么JS里有了Array,还要自己实现ArrayList1.1 很多人忽略的事实:JS的Array不是传统意义上的数组看到这个标题,第一反应大概率是:JS的Array本身就是动态数组,push、pop、splice全都有,为什么还要自己写…

作者头像 李华
网站建设 2026/10/20 11:09:52

springboot医院挂号管理系统80460-计算机课程设计、毕业设计

前言 ✨ 博主介绍:一线全栈工程师,毕设实战引路人。技术栈覆盖Java、Python、C#、PHP、Node.js及UniApp跨端开发,擅长多语言项目落地与架构设计。持续分享毕设源码、开题报告、技术选型心得与职场踩坑经验。用工程化思维写代码,帮…

作者头像 李华
网站建设 2026/10/20 11:09:08

Voicebox 声音克隆与生成安装避坑指南:6 个高频卡点与最短解法

Voicebox 声音克隆与生成安装避坑指南:6 个高频卡点与最短解法 【免费下载链接】voicebox The open-source AI voice studio. Clone, dictate, create. 项目地址: https://gitcode.com/GitHub_Trending/voicebox1/voicebox Voicebox 是一款本地优先的开源 AI…

作者头像 李华