3. 幂等核心概念

在支付系统中,重复扣款往往不是“代码写错了”这么简单,而是网络超时、用户重试、消息重投、定时补偿等多种因素叠加的结果。要治理重复扣款,第一步不是写拦截逻辑,而是把幂等这件事拆成可落地的概念体系。本节围绕六个关键词展开:幂等定义、幂等键、唯一请求号、去重表、状态机幂等与业务幂等。

一、幂等定义:同一语义操作执行多次,结果一致

幂等(Idempotence)在支付语境下,指的是同一个业务请求,无论被提交、消费或补偿多少次,对账户余额、订单状态、资金流水产生的最终影响都相同。注意,幂等不等于“不执行”,也不等于“返回相同报文”,它约束的是业务终态。

例如用户发起一笔 100 元支付,因超时连续点击三次。若系统幂等设计正确,最终只会产生一条扣款流水、一次余额减少 100 元;后两次请求要么被识别为重复并直接返回首次结果,要么进入状态机判断后拒绝执行。幂等的本质是:用一次业务语义,抵消多次物理请求。

幂等判断的三要素:谁在请求(请求方身份)、请求什么(业务语义)、请求到哪一步(当前状态)。缺少任一要素,去重都可能误杀或漏杀。

二、幂等键与唯一请求号:把“同一件事”标识出来

要实现幂等,系统必须能判断“这两次请求是不是同一件事”。这依赖两个概念:

  • 幂等键(Idempotency Key):由业务语义抽象出的唯一标识,通常由业务类型 + 业务单号 + 关键维度组成,例如 PAY:ORDER_20240501_001。
  • 唯一请求号(Request No):调用方为每次请求生成的全局唯一编号,如 UUID 或雪花 ID,用于识别“这一次物理调用”。

两者关系容易混淆:唯一请求号标识“一次调用”,幂等键标识“一件业务”。同一笔支付被用户重试时,唯一请求号会变,但幂等键必须不变。因此工程上常见做法是:调用方传入业务幂等键,服务端再结合自身生成的请求号做双层校验。

POST /pay/create
{
  "requestNo": "req_9f3a7c21e8",
  "idempotentKey": "PAY:ORDER_20240501_001",
  "orderId": "ORDER_20240501_001",
  "amount": 10000,
  "currency": "CNY"
}

三、去重表:幂等落地的第一道闸门

去重表是幂等最常用的存储型方案。核心思路:为幂等键建立唯一索引,请求到达时先尝试插入,插入成功说明是首次请求,继续执行业务;插入冲突说明是重复请求,直接返回已有结果或拒绝。

字段说明示例
idempotent_key业务幂等键,唯一索引PAY:ORDER_20240501_001
request_no首次请求号req_9f3a7c21e8
status处理状态PROCESSING / SUCCESS / FAIL
result_snapshot首次结果快照{"code":"SUCCESS","tradeNo":"T001"}
expire_at过期时间,控制表膨胀2024-05-08 00:00:00

去重表的关键细节:唯一索引必须落在幂等键上;插入与业务执行要控制在同一事务或补偿框架内;重复请求应返回首次结果快照,而不是简单报错,否则调用方仍可能继续重试。

只靠“先查后插”存在并发漏洞:两个请求同时查询都发现不存在,随后都插入。必须依赖数据库唯一索引或分布式锁兜底,查询只能作为快速路径。

四、状态机幂等与业务幂等:从“防重”到“防错”

去重表解决的是“同一请求重复到达”,但支付系统还有更复杂的情况:不同请求号、不同幂等键,却试图对同一笔订单重复扣款。这时需要状态机幂等与业务幂等。

  • 状态机幂等:为订单定义严格状态迁移,如 INIT → PAYING → SUCCESS / FAIL。只有处于 INIT 的订单才允许发起扣款;已 SUCCESS 的订单再次扣款直接拒绝。状态迁移本身带条件更新,天然防重。
  • 业务幂等:从业务规则层面判断“这笔操作是否还有意义”。例如退款金额不能超过可退余额、同一订单同一渠道只能成功扣款一次。业务幂等是最后一道防线,能拦截语义重复但标识不同的请求。
UPDATE pay_order
SET status = 'PAYING', update_time = NOW()
WHERE order_id = 'ORDER_20240501_001'
  AND status = 'INIT';   -- 影响行数为 0 说明状态不允许,直接幂等拒绝

下面这张图展示了从请求进入到最终落库的幂等判断路径,包含去重表、状态机与业务规则三层校验。

支付幂等三层校验路径 支付请求到达 requestNo + idempotentKey 第一层:去重表 唯一索引插入 / 冲突检测 第二层:状态机幂等 INIT → PAYING → SUCCESS 第三层:业务幂等 金额 / 渠道 / 可退余额校验 执行扣款并落库 写流水 / 更新余额 / 记录结果 重复请求拦截 返回首次结果快照 异常与补偿 超时 / 重投 / 对账修复

五、常见误区与复盘

在实际项目中,幂等设计常踩以下坑:

  1. 把唯一请求号当幂等键:调用方每次重试都生成新请求号,去重表永远不冲突,重复扣款照样发生。
  2. 去重表与业务表不在同一事务:去重记录写入成功但业务执行失败,后续重试被误判为重复,导致漏单。
  3. 只做接口层幂等,不做状态机幂等:不同入口、不同请求号仍可能对同一订单重复扣款。
  4. 忽略结果快照:重复请求返回“处理中”或报错,调用方继续重试,放大系统压力。
  5. 去重表无过期策略:数据无限膨胀,查询与写入性能持续下降。
复盘建议:每次重复扣款事故后,按“请求标识是否稳定 → 去重是否原子 → 状态迁移是否带条件 → 业务规则是否兜底”四步回溯,通常能快速定位是哪一层幂等失效。

总结来看,幂等定义是目标,幂等键与唯一请求号是标识手段,去重表是存储闸门,状态机幂等控制流程,业务幂等守住语义底线。五者配合,才能把重复扣款从“事后发现”变成“事前拦截、事中可控、事后可查”。