← 返回题目列表

什么是幂等性?分布式系统中如何保证幂等?

高频 中等 第 8 / 27 题 更新于 2026/10/03
幂等去重重试
学习 AI 实战项目

简化版

幂等性指同一业务意图执行一次或多次,业务副作用与执行一次等价;每次响应文本不必完全相同。分布式系统无法避免超时重试和消息重投,因此下单、扣款、库存、回调等写操作要用稳定业务键识别重复,并让“去重判定、业务写入、结果记录”处在可靠的原子边界内。

详细版

幂等设计的关键是找到“同一个请求”的稳定标识,比如订单号、支付流水号、请求 ID、消息 ID。然后用可靠机制阻止重复副作用:

方案原理适用场景
唯一索引对业务唯一键建唯一约束防重复创建订单、流水
去重表处理前插入 request_id,重复插入失败MQ 消费、回调处理
Token提交前发一次性 token,提交时原子校验删除表单重复提交
状态机只允许合法状态流转订单支付、退款、发货
乐观锁更新时带 version 或条件防重复更新、并发更新
Redis 原子操作SET NX、Lua 原子校验短窗口削峰或防重复提交,核心副作用仍需持久约束

注意“先查有没有,再插入/更新”不是可靠幂等,因为并发下两个请求可能同时查到没有。幂等判断要落在唯一约束、条件更新、Lua、事务等原子机制上。

完整版教学

一、为什么分布式系统离不开幂等

分布式调用最大的特点是结果不确定。你调用支付服务超时,可能支付服务没收到,也可能扣款成功但响应丢了。为了避免误判失败,调用方通常会重试;MQ 为了保证消息不丢,也常采用至少投递一次,这意味着消费者可能收到重复消息。重试和重投是可靠性的朋友,却是重复副作用的来源。

如果接口不幂等,重试一次可能变成重复扣款、重复发货、重复创建订单。幂等就是给重试兜底,让系统敢重试,又不会重复产生业务影响。

二、幂等关注的是副作用,不是返回值

同一个请求第一次返回“创建成功”,第二次返回已保存的订单结果,响应元数据可能不同,但系统只有一张订单且只扣一次款,所以业务效果仍然幂等。反过来,接口每次都返回 200,却重复发券或重复记账,绝不算幂等。

这点很重要,因为很多接口第二次调用时天然无法返回完全一样的上下文。工程上只要第二次不会再次改变业务状态,通常就可以认为幂等。

三、幂等 key 必须稳定且有业务含义

没有幂等 key,就不知道两个请求是不是同一个操作。随机 UUID 只有在一次业务意图创建时生成、所有重试都复用才有效;每次尝试重建 UUID 或时间戳都会绕过去重。服务端还应保存请求参数摘要,同一 key 携带不同金额或收款方时必须报冲突,不能复用旧结果掩盖调用错误。

对于支付回调,要用第三方交易号或商户订单号;对于 MQ 消费,要用消息 ID 或业务事件 ID;对于下单,要用订单号或提交 token。

四、几种常用方案怎么落地

唯一索引是最硬的兜底。订单表对 order_no 建唯一索引,重复插入会失败,服务捕获唯一键冲突后查询并返回已有订单。它并发安全,因为约束在数据库里。

方案原子边界适用范围主要故障边界
业务表唯一键业务写本身创建订单、账务流水需把唯一键定义到正确业务作用域
去重表 + 本地事务去重记录与业务更新MQ 消费、回调跨外部系统副作用仍不能原子提交
状态机条件更新当前状态与目标状态支付、退款、发货必须拒绝非法逆向迁移
Redis TokenToken 消费表单防连点、短窗口削峰过期、故障切换与业务失败后的恢复

去重表适合消息消费。消费者先在 processed_message 表插入 message_id,插入成功才执行业务,插入失败说明处理过。关键是去重记录和业务更新最好放在同一个本地事务里,否则可能业务成功但去重记录失败,下一次又重复执行。

BEGIN;
INSERT INTO processed_message(consumer, event_id)
VALUES ('points-service', 'evt-9001'); -- 联合唯一键
INSERT INTO points_ledger(user_id, event_id, amount)
VALUES (42, 'evt-9001', 100);          -- event_id 同样唯一
COMMIT;
-- 重复事件在唯一约束处失败,不能再次增加积分

Token 适合减少表单重复提交。用户进入提交页先申请 token,提交时用 Lua 原子消费;但 token 被消费后若业务事务失败,还要有明确失败状态或重新申请流程。Redis 数据丢失、主从切换和 token 过期也决定它不应成为核心账务唯一防线,最终仍应依赖业务唯一约束或账务流水。

状态机适合订单类业务。订单只能从待支付到已支付,再到已发货。重复支付回调到来时,发现订单已经已支付,就直接返回成功,不再重复发积分或发消息。

五、分布式锁不是幂等的替代品

分布式锁能让同一时刻只有一个请求处理,但锁释放后,重复请求再次到来仍可能再次执行。它解决的是并发互斥,不解决历史去重。正确设计通常是锁 + 状态/去重配合:锁降低并发冲突,去重表或状态机保证重复请求不产生第二次副作用。

六、面试常见坑

先查后写是最典型坑:两个请求同时查都没有,然后都插入成功。另一个坑是幂等记录和业务操作不在同一个事务中,导致中间失败后重复执行。还有一个坑是幂等 key 粒度过粗或过细,过粗会误杀不同请求,过细会挡不住重复请求。

七、去重窗口、失败状态和存储成本

幂等记录要保留多久取决于最大重试窗口、消息保留期和人工重放周期。若消息可能在 7 天内重放,而去重记录只保存 1 小时,第 2 天的重放仍会重复生效;对永久唯一的支付流水,直接让业务表唯一键长期承担约束通常比短 TTL 去重缓存更可靠。

假设每天处理 1 亿个事件,每条去重记录连索引约 200B,保留 7 天的原始量约为 100000000 × 200 × 7 = 140GB,还未计算副本。可以按日期分区、归档或复用业务结果表,但不能通过缩短窗口把重复风险偷偷转嫁给业务。

PROCESSING 记录也不能永久卡住。恢复任务应按业务键查询真实副作用:确认已完成就补写结果,确认未执行才安全重试,无法判断则进入人工处理;直接删除记录再执行会把未知状态变成重复副作用。

记忆钩子:幂等的安全边界不是“挡住并发那一刻”,而是覆盖这次业务意图可能被重新送达的整个生命周期。

八、常见误区与追问

  • 误区:幂等就是接口不报错。 幂等关注同一业务意图的副作用等价,不是吞异常或一律返回成功。
  • 误区:前端禁止重复点击就够了。 网络重试、网关重放、MQ 重投和服务端恢复仍会产生重复请求。
  • 误区:先查去重表再执行业务即可。 检查与占位必须原子,去重状态与本地业务写也应在同一事务提交。
  • 误区:Redis SET NX 能独立保护核心账务。 过期、故障切换和持久性边界仍存在,核心事实需要数据库唯一约束或账务状态机兜底。
  • 追问:同一幂等键参数不同怎么办? 比较规范化请求摘要并返回冲突,不能继续执行,也不能静默返回旧结果。
  • 追问:幂等记录一直处于处理中怎么办? 查询业务事实或外部系统结果,由状态机接管,未经确认不能删记录重做。
  • 追问:幂等和分布式锁有什么区别? 锁只限制某段时间的并发进入,幂等还要识别锁释放后再次到达的历史重复。

九、加强记忆

幂等就是让同一业务操作重复执行也只产生一次副作用。分布式里因为超时、重试、MQ 重投无法避免,所以写接口必须幂等。核心前提是稳定业务唯一标识,核心手段是唯一索引、去重表、Token、状态机、乐观锁和原子操作。记住两条底线:先查后写不可靠,分布式锁不等于幂等。