至少一次投递意味着消息会重复。重复到底从哪来?redelivered 标志能不能识别?RabbitMQ 会不会帮你去重?消费者怎么做才算幂等?这篇先复现两种重复,再实测一个最小的幂等实现。
消费者 A 处理了 order-1001,redelivered=False 消费者 B 处理了 order-1001,redelivered=True 同一条订单被业务处理了 2 次
处理完业务、ack 之前崩溃(或 ack 丢失),broker 会重投该消息,标志 redelivered=True。
第一次发布(msg-2)的结果: StreamLostError(发布端据此认为“没发成功”)
这时队列里实际已经有: 1 条
发布端重连后重发同一条(同样的 message_id=msg-2)并收到 Ack;队列里现在有: 2 条
取出来: [('order-1002', False, 'msg-2'), ('order-1002', False, 'msg-2')] ——两份都是 redelivered=False:redelivered 标志抓不住发布端造成的重复消息已到 broker 但 Ack 没回来,发布端重试,队列里就有两份;这两份的 redelivered 都是 False,所以不能靠这个标志去重。
收到 3 条(message_id 只是一个普通属性,RabbitMQ 不会用它去重)
message_id 只是属性,RabbitMQ 不用它去重。
朴素消费者:收到 1100 次投递,业务执行 1100 次,余额 = 1100(应为 1000) 幂等消费者(message_id 唯一约束):收到 1100 次投递,业务执行 1000 次,余额 = 1000 耗时: 朴素 36 ms,幂等 37 ms(幂等版多一次 insert,sqlite 内存库,两者差别在噪声内)
用 message_id(或业务上唯一的键)做唯一约束,和业务操作放在同一个数据库事务里;重复的消息插入失败,业务就不会再执行。
100 万个 36 字符的 message_id:插入 787 ms,库大小约 28.3 MiB(sqlite,without rowid)
去重记录要保留到重复可能出现的窗口之外,100 万个 id 约 28 MiB,需要定期清理。