分布式理论05:幂等性设计

从重复提交、消息重投、接口重试这些真实场景出发,理解幂等键、状态机、唯一约束和去重表这些落地手段。

字数 1545 阅读时长 ≈ 5 分钟 2026-5-31 2026-7-12
分布式理论05:幂等性设计

为什么需要幂等

在分布式系统里,重复是常态,不是例外。

用户在前端点了两下提交;网关超时后重试了一次;MQ 消费失败后消息重新投递;定时任务因为漂移重复执行;下游调用超时,但实际上已经执行成功。

这些情况本质上都是同一个:同一个业务意图被执行了多次。

幂等要解决的就是这件事:同一个请求执行一次和执行多次,结果应该一样。不是说接口返回一样就算幂等,而是业务后果要一样。扣一次钱和扣两次钱,即使都返回成功,也不是幂等。

幂等不是一个注解能解决的

很多人以为幂等就是加个幂等框架、加个注解、检查一下请求 ID。真实系统里没这么简单。

原因是幂等要区分:

维度说明
重复来源前端重复点击、网关重试、MQ 重投、任务重跑
重复间隔毫秒级并发重复、秒级重试、分钟级补偿
业务后果写数据、扣钱、发消息、触发外部动作
接受代价接受重复读、不能接受重复扣减、可接受重复通知

不同场景的幂等手段完全不同。把所有接口都套同一个通用幂等方案,要么性能扛不住,要么边界没覆盖。

四类常见幂等手段

唯一键约束

最直接的幂等手段,是让数据库替你拦一次。

比如订单支付流水:

alter table pay_flow add unique key uk_out_trade_no (out_trade_no);

外部交易号重复时,数据库直接拒绝插入。业务捕获到唯一键冲突后,可以查询已有记录并返回成功。

这个办法的好处是简单、可靠、在数据库层面兜底。局限是只能防同一类重复,且要求业务上有明确的唯一键。

状态机推进

有状态流转的业务,天然适合做幂等。因为状态只允许向特定方向推进:

待支付 → 支付中 → 支付成功
        ↘ 支付失败 ↗ 重试

写状态时用条件更新:

update orders
set status = 'PAID', pay_time = now()
where order_id = ?
  and status = 'PAYING';

如果状态已经是 PAID,更新影响行数为 0,说明已经处理过,返回成功即可。

TCC、订单状态机、审批流,基本都靠这个思路。它不依赖额外的去重表,状态本身就是幂等依据。

去重表 / 幂等表

当业务本身没有天然唯一键,或者状态不那么明确时,可以引入一张去重表。

deduplication
  - biz_type    业务类型
  - biz_id      业务唯一标识
  - status      PROCESSING / SUCCESS / FAILED
  - created_at  创建时间

处理流程大概是:

  1. 先尝试插入去重记录,标识为处理中
  2. 插入成功则执行业务
  3. 业务完成后更新去重记录为成功
  4. 插入失败则说明重复,返回已有结果

这里要注意的是:插入去重表和执行业务不在同一个事务里时,要考虑进程挂在中间的情况。所以通常还需要配合超时、重试、状态回查。

天然幂等的操作

不是所有操作都要额外设计幂等。有些操作天然就是幂等的:

  • 查询:读操作不改变状态
  • set 某个字段为固定值set status = 'DONE' 执行多次结果一样
  • delete by id:删过一次之后再删,结果都是已删除

真正麻烦的是”加 1”、“扣减”、“创建新记录”、“发送通知”这类会累积或产生副作用的操作。

常见场景怎么选

场景推荐幂等手段原因
订单创建前端传 requestId + 后端去重表避免用户重复提交
支付回调外部交易号唯一键 + 状态机外部系统必然会重试
MQ 消费消息业务主键 + 去重表/唯一键消息中间件本身就保证 at-least-once
库存扣减条件更新 + 版本号/状态扣减类操作最怕重复
定时任务分布式锁 + 条件更新任务漂移和重跑都可能触发重复

没有一种手段覆盖所有场景。真实项目里经常是多种手段叠加:数据库唯一键兜底、状态机控制流转、幂等表记录处理状态。

读幂等也容易被忽略

幂等不只发生在写路径,读路径也有坑。

比如查询订单列表,如果分页用 offset,在数据频繁写入时,第二页可能和第一页出现重复或漏掉数据。这也是一种”重复请求结果不一致”。

更稳的做法是用基于游标或时间戳的分页:

select * from orders
where created_at < ?
order by created_at desc
limit 20;

这类”读幂等”不涉及扣钱,但会影响用户体验和数据同步准确性。

常见误区

误区一:加了请求 ID 就叫幂等

请求 ID 只是入口去重。如果业务内部还会调用下游,下游的幂等仍然要自己保证。

误区二:幂等就是去重,不影响业务设计

幂等不是事后补丁。业务模型里如果没有唯一键、没有状态机、没有版本号,幂等很难做好。幂等能力是系统设计的一部分。

误区三:只防并发重复,不防延迟重复

有些系统用 Redis 做短时间去重,过期后就不防了。但任务补偿、消息重投可能在很久之后到来。要根据业务重复窗口设计去重保留时间。

误区四:重复请求一律返回成功

有些重复是合理的,比如查询;有些重复是异常的,比如同一分钟内 100 次同一笔支付。后者要告警,不能默默返回成功。

设计时先问这些问题

  1. 重复从哪里来?是前端重复点击、接口重试,还是消息重投?
  2. 重复窗口有多长?秒级、分钟级,还是跨天?
  3. 业务上有没有天然唯一键?
  4. 有没有状态机可以利用?
  5. 重复执行会造成什么后果?扣钱、重复发券、还是只是浪费计算?
  6. 数据库层有没有兜底约束?

想清楚这些,幂等方案才不会漏。