先把当时的情况摆出来。Kafka 消费者处理订单事件,下游写库时偶发 Duplicate entry for key uk_order_no,重试三次后进死信队列,追日志发现同一事件在十分钟内投递了两次,消费位点提交在业务处理之后。

排查方向其实有限,先把可能性排一排。至少一次语义下重复投递属于正常现象。还有一种是消费位点在业务写库成功之前就提交,重启后重放。

另外,唯一索引冲突被当成异常抛出,触发了重试与死信。少数情况下是消费者组再平衡时分区被重复分配,同一批消息被两个实例处理。

先确认重复事件的时间间隔与位点

python -c "print({'order_no':'20261111001','first':'10:02:11','second':'10:09:47'})"

接着把唯一索引冲突识别为已处理而不是失败

python -c "print('except IntegrityError: treat as duplicate and commit offset')"

随后改成先落库再提交位点

python -c "print({'commit':'after_commit','enable_auto_commit':False})"

末了用事件 id 建一张去重表

psql -d order -c "INSERT INTO event_dedup(event_id) VALUES('evt-8821') ON CONFLICT DO NOTHING RETURNING event_id;"

恢复的判定其实很直接。重复投递的事件被识别并跳过,死信队列里不再出现唯一索引冲突,订单表条数与事件去重后的数量一致。

幂等要落在消费端,靠消息中间件不重发是不现实的,唯一索引加去重表是最省心的组合。同一份配置在测试环境跑通,不代表线上没事,两边的默认值经常不一样。

作者 9823189