跳到主要内容

消息幂等性

消息幂等性让同一业务动作重复到达时保持结果一致。本文负责去重、状态转换和乱序处理。Kafka 示例采用 4.1 的普通消费者组。

1. 面试核心​

先记住这几条,再读正文
  • 事务内去重。 去重记录与业务变化在同一本地事务提交,才能共同成功或回滚。卡 · 综合题
  • 幂等键。 幂等键标识业务动作,生产者重试时应复用原键。
  • Kafka 事务范围。 Kafka 事务可原子提交输出记录与消费 offset,外部数据库需另行协调。
  • 版本与乱序。 完整快照可用单调版本跳过旧值,增量事件需校验连续性。卡 · 版本号
  • 处理顺序。 同一业务键固定分区,并在消费者内部串行处理,才能保持处理顺序。

2. 处理与确认之间的故障窗口​

业务提交后、确认前崩溃,会导致消息重投。确认先于业务提交则存在漏处理窗口,详见消息可靠性。

消费者应把重复动作当作可预期输入。恢复流程根据持久化状态判断是否已完成。

3. 幂等键​

幂等键可以是 event ID 或 command ID。重试同一动作时复用原键。不同动作即使内容相同,也应使用不同键。

BEGIN;
INSERT INTO consumed_event(consumer_name, event_id)
VALUES ('accounting', 'event-1001');
-- 在同一事务中执行记账更新。
COMMIT;

示例表需对 (consumer_name,event_id) 建唯一约束。INSERT 成功后才执行记账,任一步失败都回滚。并发重复插入由数据库约束协调。

只有该去重约束的冲突,才可据此核对已有结果。其他唯一约束错误仍属于业务失败。重复消息还应核对载荷,避免同一键对应不同动作。

去重保留时间至少覆盖可重放窗口。清理过早会让旧事件再次生效。跨消费者的处理记录应带消费者作用域。

4. 幂等的状态转换​

有些业务可直接使用条件状态转换:

UPDATE orders
SET status = 'PAID', paid_at = ?
WHERE order_id = ? AND status = 'PENDING';

首次处理从 PENDING 转成 PAID。重复执行影响 0 行。此时还需读取结果,区分已完成、记录缺失和非法状态。

金额累加需要唯一业务流水或去重记录。调用外部支付时,应传递稳定幂等键。跨系统结果不明时,通过查询或对账恢复。

5. Exactly-once 的范围​

Kafka 4.1 事务把输出记录与消费 offset 原子提交。消费者设置 isolation.level=read_committed。对事务消息,它只返回已提交的记录。非事务消息仍正常返回。失败事务的输出对这种消费者不可见。

事务消费者重放输入时,会重新计算结果。成功提交只发布一组输出及对应进度。生产者配置稳定且独占的 transactional.id。批次开启事务,写出结果后调用 sendOffsetsToTransaction。随后提交事务,共同发布结果与进度。

MySQL 提交和外部 API 不属于这段 Kafka 事务。外部结果需本地事务去重、Outbox 或下游幂等协议。幂等生产者本身也只处理生产重试的重复写入。

6. 按业务键保证顺序​

同一订单的事件可固定写入同一 partition。普通消费者组对该分区分配单一消费者。内部再分发到工作线程时,也应按业务键串行。

增加分区会改变部分键的映射。迁移时应处理旧分区存量与新分区事件的衔接。跨生产者的发送顺序还需由业务版本定义。

RabbitMQ 多消费者和 requeue 会改变完成顺序。严格按键处理时,需约束同一键的在途任务。毒消息是否阻塞后续事件,也属于业务规则。

7. 版本号​

完整快照可以用条件更新阻止旧值覆盖:

UPDATE search_projection
SET payload = ?, version = 3
WHERE aggregate_id = ? AND version < 3;

示例假设记录已经存在,payload 是版本 3 的完整快照。首次创建还需要唯一键保护的插入路径。版本 2 随后到达时,条件更新会跳过它。

增量事件需要另一种处理。例如版本 2 是加 10,版本 3 是减 5。直接跳过版本 2 会丢失加 10 的效果。消费者应要求 expected_version 匹配,或先补齐缺口。

权威写入端生成单调版本,更适合表示业务顺序。墙上时钟会偏移,相同时间精度内也会重复。

8. 重新均衡与超时​

Kafka 的 max.poll.interval.ms 限制 poll 间隔。处理过久会引发成员失效及分区重分配。静态成员的转移时机还受会话超时影响。

失去分区后,旧任务仍有机会完成外部写入。数据库去重和版本约束需独立生效。撤销时停止领取新任务,并协调未完成工作。长任务可先持久化为独立作业,再确认创建结果。

9. 结果核对​

监控去重命中、版本跳过、非法状态和处理延迟。去重命中突增时,检查生产重试和消费重启。关键业务还需比较源事件与最终流水。

幂等记录表达已完成动作。它的保留、载荷校验和清理都要纳入恢复方案。

10. 常见问题​

10.1 进程内互斥与持久化去重​

synchronized 只协调当前进程内的线程。持久化去重记录覆盖多实例和重启后的重放。单机互斥可以作为局部优化。

10.2 单线程消费与端到端顺序​

11 · 面试题卡片​

11.1 如何保证消息幂等消费,并处理重复与乱序​

出现公司:京东、字节跳动

题型:综合题

考察重点

  • 业务提交与 ack、offset 提交之间的故障窗口。
  • 去重记录、条件状态转换和本地事务。
  • partition 顺序、版本号与 exactly-once 的作用范围。

相关内容:处理与确认之间的故障窗口至结果核对。

参考回答

重复消息通过稳定业务键去重,乱序消息按业务语义串行处理或校验版本。

去重记录与业务变化在同一事务提交;条件状态转换也能让重复操作保持结果一致。

同一业务键应固定分区,并在消费端串行处理。完整快照可跳过旧版本,增量事件则要补齐缺口或校验预期版本。Kafka 事务覆盖输出与 offset,外部数据库等副作用需另行协调。

11.2 版本 3 先于版本 2 到达,可以直接丢弃版本 2 吗?​

出现公司:暂无已核验的公开来源(本站自测)

考察重点:完整快照与增量事件的恢复差异。

相关内容:版本号。

参考回答

完整快照可保留版本 3,并跳过旧快照。

增量事件则需补齐缺口,否则会丢失版本 2 的效果。例如先减 5 再跳过加 10,结果就错误。增量处理应校验预期版本,或暂存并补取前序事件。

12. 参考资料​