开放问答
Kafka 消费侧 Redis 幂等占坑与 MySQL 业务写入跨事务,如何堵住「写业务成功但占坑失败」的重复窗口
该问题的正文、回答与评论。
提问者 谢望舒 · 状态 open
Kafka幂等Redis消息中间件跨事务一致性
【背景】
我们用 Kafka 3.6(3 broker,topic order-paid 32 分区)承载订单支付成功事件,消费组 12 实例(每实例 2~3 分区),日均约 800 万条,峰值 6500 msg/s。消费逻辑如下:
GET dedup:{msg_id}→ 命中则跳过(幂等)- 写 MySQL 订单表(
REQUIRES_NEW,独立本地事务,含状态 CAS 更新) SET dedup:{msg_id} 1 EX 604800(Redis 7.x,去重窗口 7 天)- 提交 Kafka offset
幂等去重走 Redis 是为了性能(日均 800 万 × 7 天 ≈ 5600 万条,放 MySQL 去重表担心锁竞争),但步骤 2 和 3 天然不在同一个事务里。
【现象 / 卡点】
- 步骤 2 成功、步骤 3 因 Redis 网络抖动(P99 偶发 3s+)写入失败 → offset 未提交 → 消息重新投递 → 步骤 1 GET 不到 → 步骤 2 再次写 MySQL → 重复落库,且因为步骤 2 是 CAS 更新(
WHERE status='CREATED'),第二次 affected rows=0,业务上"静默吞掉"了,不报错但数据不一致。 - 如果把 SET NX 挪到步骤 2 之前(先占坑再写业务),那步骤 2 失败时 Redis 里已有去重记录,消息不会重试 → 丢消息。
- 用 MySQL 去重表替代 Redis 可以把 2 和 3 合进一个本地事务,但 5600 万行 INSERT 的锁竞争在高峰开始明显(InnoDB 行锁等待 P99 从 1ms 飙到 40ms+)。
【已尝试的排查与结果】
- 把 SET NX 提前到写业务之前,加一个 5s 的"占坑 TTL",业务失败后主动 DEL → 逻辑上可行,但 DEL 本身也可能失败,需要补偿任务扫 Redis 清理过期占坑,引入了额外复杂度;
- 换 MySQL 去重表(UNIQUE KEY: msg_id),和订单表同事务 → 功能正确,但 5600 万行后 INSERT 的 gap lock 竞争让同分区的其他事务 P99 涨了 3 倍;
- 用业务表自身的 UNIQUE KEY 做天然幂等 → 对 INSERT 类操作可行,但我们这条链路是 UPDATE(状态机推进),没法靠 UNIQUE KEY 挡重复。
【期望得到的答案形态】
- 「先占坑再写业务 + 占坑失败自动清理」这个模式,有没有更轻量的实现(比如 Lua 脚本原子占坑 + 业务失败回调 DEL),避免引入补偿任务?
- 去重表 5000 万+ 行时,除了分表/分区,有没有别的思路(比如按月分表 + 定时归档,或者 Redis + 本地事务双写 + 对账兜底)?
- 对于 UPDATE 类操作(非 INSERT),幂等 key 怎么设计才既防重复又不误伤合法的状态回退(比如售后场景下状态可能从 SHIPPED 回退到 REFUNDING)?
- 大家生产上"Redis 占坑 + MySQL 写业务"这个跨事务窗口,最终是怎么兜底的?
评论
暂无评论。