资讯动态

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

发布时间:2026/10/20 11:12:42 来源:尧图企业网站定制
消息队列的重复消费与顺序订单消息明明只发了一条订单却被创建了两次。日志里 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,idempotentTrue):self.processed{}# 幂等表消息 id - 结果self.count0# 业务真正执行的次数self.idempotentidempotentdefhandle(self,msg_id,payload):ifself.idempotentandmsg_idinself.processed:print(f [{msg_id}] 重复投递幂等表拦下)returnself.count1self.processed[msg_id]payloadprint(f [{msg_id}] 真正执行{payload})defredeliver(consumer,msg):模拟处理完没来得及 ack 就挂broker 重投同一条。msg_id,payloadmsg consumer.handle(msg_id,payload)print(f [{msg_id}] 没来得及 ack 就挂了broker 重投)consumer.handle(msg_id,payload)print( 重复现场 )rawConsumer(idempotentFalse)redeliver(raw,(m-1,扣库存 1 个))print(f裸消费业务执行{raw.count}次库存被扣了两次)safeConsumer(idempotentTrue)redeliver(safe,(m-1,扣库存 1 个))print(f幂等消费业务执行{safe.count}次)defapply_action(state,action):订单状态机创建 - 支付 - 退款乱序直接拒绝。rules{(init,创建):created,(created,支付):paid,(paid,退款):refunded,}nxtrules.get((state,action))returnnxtifnxtelsef拒绝({state}-{action})print(\n 顺序现场 )flow[创建,支付,退款]stateinitprint(分区有序,end )forainflow:stateapply_action(state,a)print(f{a}-{state},end )print()shuffledflow[:]random.shuffle(shuffled)stateinitprint(并发乱序,end )forainshuffled:stateapply_action(state,a)print(f{a}-{state},end )print()print(结论顺序靠分区重复靠幂等。)适用边界这一篇管重复与顺序两件事。消息积压、事务消息的实现细节半消息、回查、以及 Kafka 幂等生产者和事务 API 的机制是另一个题。去重表的键设计随业务变一般是业务单据号而不是消息 id因为同一个业务动作可能产生多条消息。跑完演示对照你线上的消费端先问一句同一条消息进来两次我的代码结果一样吗。答案是「不确定」的地方就是下一个要修的 bug。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑