分布式理论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 创建时间
处理流程大概是:
- 先尝试插入去重记录,标识为处理中
- 插入成功则执行业务
- 业务完成后更新去重记录为成功
- 插入失败则说明重复,返回已有结果
这里要注意的是:插入去重表和执行业务不在同一个事务里时,要考虑进程挂在中间的情况。所以通常还需要配合超时、重试、状态回查。
天然幂等的操作
不是所有操作都要额外设计幂等。有些操作天然就是幂等的:
- 查询:读操作不改变状态
- 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 次同一笔支付。后者要告警,不能默默返回成功。
设计时先问这些问题
- 重复从哪里来?是前端重复点击、接口重试,还是消息重投?
- 重复窗口有多长?秒级、分钟级,还是跨天?
- 业务上有没有天然唯一键?
- 有没有状态机可以利用?
- 重复执行会造成什么后果?扣钱、重复发券、还是只是浪费计算?
- 数据库层有没有兜底约束?
想清楚这些,幂等方案才不会漏。