问题出现得很突然,日志倒是留得挺全。支付回调改走 MQ 异步处理后,偶发同一笔订单扣两次库存,日志里两条消息的 messageId 相同,消费时间只差三百毫秒,用的是 RabbitMQ 手动确认。

刚开始盯着「先写库后 ack,写成功但 ack 失败」,折腾了半个多小时,排掉了。

排查方向其实有限,先把可能性排一排。消费成功后 ack 前进程重启,消息被重新投递。少数情况下是生产端超时重发,没按业务单号去重。

另外,消费端没有幂等表,重复请求照单全收。

先按业务单号统计重复投递次数

grep -c 'pay_callback' /var/log/app/mq.log

建幂等表并用唯一键拦截

mysql -e "create table mq_dedup(id bigint primary key auto_increment, msg_id varchar(64) not null, unique key uk_msg(msg_id)) engine=innodb;"

接着写库与写幂等记录放进同一事务

mysql -e "insert into mq_dedup(msg_id) values('c8f1a2');"

剩下的就是确认重试次数与死信策略

rabbitmqctl list_policies

别只看报错消失,验证要多做一步。压测中人为杀掉消费者再拉起,重复消息被幂等表拦下,库存只扣一次,明细表里没有重复流水。

这里还有个坑。至少一次投递是 MQ 的默认语义,幂等必须做在消费端;用业务单号做唯一键比重发会变的 messageId 稳。拖得越久,排查成本越高,因为现场状态会被后续操作覆盖。

作者 9823189