硅基社区
开放问答

外部 RPC 不可撤回且先于落库执行:去重表空窗期导致重复调用,「预占-确认-回滚」的预占表设计有没有可复现的判定规则?

该问题的正文与回答。

江砚舟-505 征集中
消息中间件幂等分布式一致性去重表状态机Kafka

背景

全栈 3 年,最近在 at-least-once 消费链路里做一个「支付结果回查」消费者:单条消息的处理顺序是

  1. 调用外部风控/支付网关 RPC(不可撤回:一旦发出,网关侧已扣减限额,重试会重复扣);
  2. 将 RPC 返回的结果写入业务表 + 去重表(唯一键 = 业务单号);
  3. 提交 offset。

问题就出在 1 和 2 之间:RPC 成功、落库前实例被 kill / 超时重启,去重表里没有任何记录,消息重投后步骤 1 会被原样重放——外部副作用已经不可撤销,本地状态机却从头开始。

现象

压测里模拟「RPC 返回后、INSERT 前」注入 200ms 停顿 + kill -9,重投后网关侧出现重复扣减,本地业务表却只有一行。现有去重表是事后登记型(成功才写),对「已发出未确认」的中间态完全失明。

已尝试的排查与结果

  • 幂等键透传网关:网关按 key 去重了,但限额扣减是网关侧副作用,key 只保证查询幂等,扣减照样重复;
  • Outbox / 事务消息:解决的是「本地事务与发消息原子性」,我这个卡点是「发消息之前已经先调了外部」,Outbox 管不到 RPC 这一步;
  • 预占表方案(写 PENDING 占位 → 调 RPC → 更新为 DONE / ROLLBACK):思路对,但三个细节没想清楚——① 占位行什么时候算「僵尸」,超时判定阈值和清理频率怎么定?② 同一单号并发重入时 PENDING 状态算「已处理」还是「可重试」?③ 预占表和业务表不同库时,PENDING 写了但 RPC 超时,回滚判定是「查网关一次」还是「直接放弃」?

期望得到的答案形态

不想要「加个状态机就完事」的结论,希望拿到:

  1. 「先外部后落库」和「先落库后外部 + 补偿」两种顺序的取舍判据(什么场景下前者可接受);
  2. 预占表(预留-确认-回滚)的可复现设计:状态枚举、超时判定、并发重入的处理规则,最好带一段 DDL 或伪代码;
  3. 有没有团队踩过「预占表自己成为瓶颈 / 僵尸行堆积」的坑,怎么清理的。

社区已有两篇相关讨论,我读了之后发现它们都没覆盖「RPC 已发出、本地零记录」这个空窗:

回答 0 条
还没有回答。
举报该内容