已采纳
回答正文
回复
讨论正文
该问题的正文与回答。
15 年全栈,近两年主责支付/结算链路的消息消费端。Kafka 3.4(3 broker KRaft),topic settlement-event 32 分区,消费组 6 实例,日均 4200 万条。消费逻辑是单条同步处理(非攒批),每条消息内部做三步串行业务操作:
account_balance 表(UPDATE ... SET balance = balance - ? WHERE account_id = ? AND balance >= ?)ledger_entry 表(INSERT,含唯一键 biz_order_id + direction)offset 提交策略:三步全部成功后 consumer.commitSync(offset + 1)。
上个月一次 broker 网络抖动(约 12s),6 个实例中有 3 个在第三步 RPC 返回后、commitSync 前断连,broker 侧 offset 未推进。重启后从旧 offset 重读,这 3 条消息的三步全部已经执行过——但我们的幂等设计只覆盖了第二步(ledger_entry 的 biz_order_id 唯一键)。
结果:
account_balance 没有幂等键,纯 UPDATE 无 WHERE 守卫)对账脚本 T+1 才发现,人工回滚 3 条扣款 + 联系风控解冻。总损失不大,但暴露了一个设计盲区:"幂等"被分散在三个步骤里,每步各管各的,没有一个统一的"这笔业务做到哪了"的状态锚点。
方案 A:每步各建幂等键
WHERE idempotency_key = ?(需改表结构,account_balance 是高频热点表,加唯一索引怕影响写入吞吐)biz_order_id + direction 唯一键)notification_log 表加 request_id 唯一键方案 B:引入本地事务表(outbox 的变体)
biz_operation 表:(biz_id PK, status ENUM('INIT','DEBIT_DONE','LEDGER_DONE','NOTIFY_DONE','DONE'), idempotency_key UNIQUE, created_at, updated_at)account_balance 和 biz_operation 必须在同一事务——这没问题,同库)biz_operation 表会持续增长,按什么维度归档?按 created_at 定期迁移到冷表?还是按 status = 'DONE' AND updated_at < NOW() - INTERVAL 30 DAY 直接删?删除后如果有一条极晚的重读(比如 offset 卡在 30 天前)怎么办?biz_id 被两个 consumer 实例同时处理(rebalance 窗口),靠 biz_operation 的 INSERT ... ON DUPLICATE KEY UPDATE 拿锁?还是 SELECT ... FOR UPDATE?方案 C:Kafka 事务(transactional producer)+ isolation.level=read_committed
不追求"银弹",想收集不同规模团队的实际取舍:
offsets.retention.minutes 是多少?幂等键 / 状态表的保留期是否 ≥ offset retention?有没有人踩过"幂等键过期了但 offset 还在"的窗口?biz_operation 表 + 一个 CLI?还是靠 ELK 串日志?