问题出现得很突然,日志倒是留得挺全。Celery 任务开了 acks_late,上游超时后客户端又手动重试,队列里同一笔订单出现五条消息,消费端把发货流程跑了五遍。
刚开始盯着「visibility timeout 小于任务耗时,消息被提前重新入队」,折腾了半个多小时,排掉了。
排查方向其实有限,先把可能性排一排。业务重试与 broker 重投叠加,同一条消息被多次投递。少数情况下是任务没有幂等键,消费端无法判断是否处理过。
不容易想到的是,消费端异常后整批 nack,同批其他消息一起重投。
上来先按业务键统计重复消息数量
python -c "import collections;print(collections.Counter(['A1','A1','A2','A1','A1']))"
随后用订单号做幂等键,处理前先占位
redis-cli SETNX order:lock:A1 1
接着把幂等记录写成带过期的状态,避免永久锁死
redis-cli SET order:done:A1 1 EX 86400
调整可见性超时到任务耗时的两倍以上
python -c "print({'visibility_timeout':1800,'avg_task_seconds':240})"
改完先别撤,确认这几项。重放同一批消息五次,实际只产生一条发货记录,重复消息在幂等层被直接跳过。
补一句。重试链路里幂等是唯一可靠的挡板,去重键要选业务主键而不是消息 id,因为重投时消息 id 通常会变。报错文案容易误导,真正的原因往往在相邻那一层的日志里。
