为什么消费端比生产端更难
生产端只管把消息发出去,消费端却要面对"消息可能重复、可能失败、可能乱序、可能堆积"这一整串麻烦。生产端发完即走,消费端要在不可靠的投递下把业务正确落地,这才是消息队列真正的难点所在。
本文核心知识点
- 幂等重复消费不产生重复结果。
- 重试失败消息按退避策略重投。
- 死信队列多次失败的消息集中处理。
- 积压消费跟不上生产时的排查思路。
至少一次投递带来的重复
为了不丢消息,消息队列普遍采用"至少一次(at-least-once)"投递:消费端处理完还没来得及确认,进程就挂了,队列会把这条消息再投给另一个消费者。于是同一条消息被消费两次几乎是必然事件,消费端必须假设"消息可能重复"。
消费幂等怎么保证
最直接的办法是给每条消息一个唯一业务键,消费时先查"这个键处理过没有",处理过就直接确认跳过。常见做法:用消息 ID 落一张去重表、或利用业务表的唯一约束(同一订单号只能扣一次)。
// 伪代码:先占坑再处理
if (dedup.has(msg.bizId)) return ack(); // 已处理,跳过
dedup.mark(msg.bizId);
process(msg); // 处理业务
ack();
重试与退避
业务依赖的下游抖动时,立刻重试往往还是失败。应该用指数退避:第一次失败等 1 秒、再失败等 5 秒、再等 30 秒,给下游恢复时间。重试要有上限,超过上限就别一直循环,否则会把坏消息反复重投、拖垮消费能力。
死信队列:处理失败的消息
多次重试仍失败的消息,放进专门的死信队列(DLQ),不再阻塞正常消费。死信消息可以人工排查、修好后重新投递,或写告警。别让一条坏消息卡在主队列里反复重投。
消费积压怎么排查
队列 lag 持续增长就是积压。先看是消费变慢(单条处理耗时上涨,多半是下游慢)还是消费实例不够(并行度低)。前者优化单条逻辑、异步化重操作;后者扩容消费实例、提高分区并行度。
顺序消费
同一实体(如同一个订单)的消息要求按先后消费,就把它固定路由到同一个分区/队列,由单线程顺序处理。代价是牺牲并行度,只对必须保序的实体做,别全局串行。
落地清单
- 默认消息会重复,消费端必须幂等。
- 失败用指数退避重试,设上限。
- 超限消息进死信队列,人工兜底。
- 盯队列 lag,积压先定位是慢还是少。
- 同一实体保序,别全局串行。