什么是幂等性?分布式系统中如何保证幂等?
简化版
幂等性指同一个操作执行一次和执行多次,对系统产生的最终影响相同。分布式系统里超时、重试、MQ 重投、用户重复点击都很常见,所以写接口必须幂等,尤其是下单、扣款、扣库存。常见方案有业务唯一键、唯一索引、去重表、Token、状态机、乐观锁和原子操作。
详细版
幂等设计的关键是找到“同一个请求”的稳定标识,比如订单号、支付流水号、请求 ID、消息 ID。然后用可靠机制阻止重复副作用:
| 方案 | 原理 | 适用场景 |
|---|---|---|
| 唯一索引 | 对业务唯一键建唯一约束 | 防重复创建订单、流水 |
| 去重表 | 处理前插入 request_id,重复插入失败 | MQ 消费、回调处理 |
| Token | 提交前发一次性 token,提交时原子校验删除 | 表单重复提交 |
| 状态机 | 只允许合法状态流转 | 订单支付、退款、发货 |
| 乐观锁 | 更新时带 version 或条件 | 防重复更新、并发更新 |
| Redis 原子操作 | SET NX、Lua 校验删除 | 短期请求去重 |
注意“先查有没有,再插入/更新”不是可靠幂等,因为并发下两个请求可能同时查到没有。幂等判断要落在唯一约束、条件更新、Lua、事务等原子机制上。
完整版教学
一、为什么分布式系统离不开幂等
分布式调用最大的特点是结果不确定。你调用支付服务超时,可能支付服务没收到,也可能扣款成功但响应丢了。为了避免误判失败,调用方通常会重试;MQ 为了保证消息不丢,也常采用至少投递一次,这意味着消费者可能收到重复消息。重试和重投是可靠性的朋友,却是重复副作用的来源。
如果接口不幂等,重试一次可能变成重复扣款、重复发货、重复创建订单。幂等就是给重试兜底,让系统敢重试,又不会重复产生业务影响。
二、幂等关注的是副作用,不是返回值
同一个请求第一次返回“创建成功”,第二次返回“订单已存在”,这两个返回值不同,但系统只有一张订单,没有重复扣款,所以仍然是幂等。幂等判断的是最终状态和副作用,不要求每次响应文本完全一样。
这点很重要,因为很多接口第二次调用时天然无法返回完全一样的上下文。工程上只要第二次不会再次改变业务状态,通常就可以认为幂等。
三、幂等 key 必须稳定且有业务含义
没有幂等 key,就不知道两个请求是不是同一个操作。随机 UUID 如果每次重试都重新生成,就没有去重效果;时间戳也不适合作为幂等 key。正确做法是让客户端或上游为一次业务操作生成稳定 ID,比如 orderNo、paymentNo、 equestId、messageId,重试时携带同一个值。
对于支付回调,要用第三方交易号或商户订单号;对于 MQ 消费,要用消息 ID 或业务事件 ID;对于下单,要用订单号或提交 token。
四、几种常用方案怎么落地
唯一索引是最硬的兜底。订单表对 order_no 建唯一索引,重复插入会失败,服务捕获唯一键冲突后查询并返回已有订单。它并发安全,因为约束在数据库里。
去重表适合消息消费。消费者先在 processed_message 表插入 message_id,插入成功才执行业务,插入失败说明处理过。关键是去重记录和业务更新最好放在同一个本地事务里,否则可能业务成功但去重记录失败,下一次又重复执行。
Token 适合防重复提交。用户进入提交页先申请 token,服务端存 Redis;提交时用 Lua 原子判断 token 是否存在并删除,只有删除成功的请求继续处理。不能先 GET 再 DEL,因为并发下两个请求都可能通过 GET。
状态机适合订单类业务。订单只能从待支付到已支付,再到已发货。重复支付回调到来时,发现订单已经已支付,就直接返回成功,不再重复发积分或发消息。
五、分布式锁不是幂等的替代品
分布式锁能让同一时刻只有一个请求处理,但锁释放后,重复请求再次到来仍可能再次执行。它解决的是并发互斥,不解决历史去重。正确设计通常是锁 + 状态/去重配合:锁降低并发冲突,去重表或状态机保证重复请求不产生第二次副作用。
六、面试常见坑
先查后写是最典型坑:两个请求同时查都没有,然后都插入成功。另一个坑是幂等记录和业务操作不在同一个事务中,导致中间失败后重复执行。还有一个坑是幂等 key 粒度过粗或过细,过粗会误杀不同请求,过细会挡不住重复请求。
七、常见误区与追问
这道题不能只背概念,要把「幂等性」放回真实分布式系统里解释:谁发起、谁协调、状态如何变化、失败后怎么兜底,以及它和性能、可用性、一致性的取舍。
| 回答层次 | 要讲清的内容 | 容易漏掉的边界 |
|---|---|---|
| 核心结论 | 幂等表示同一请求执行一次和执行多次的业务结果一致,是重试、消息消费和回调的安全底座 | 不要停在名词解释 |
| 流程机制 | 客户端生成唯一请求号 -> 服务端检查去重记录或状态 -> 未处理则执行业务并落去重 -> 重复请求直接返回历史结果 -> 失败状态可按状态机补偿 | 说明触发方、参与方、状态变化和兜底 |
| 工程取舍 | 同一个 payRequestId 重试 3 次,只能扣款成功 1 次,后两次应返回已处理结果 | 分布式基础题不能只背概念,必须落到网络不可靠、节点会故障、数据有副本这些前提 |
幂等性 面试拆解:
1. 客户端生成唯一请求号
2. 服务端检查去重记录或状态
3. 未处理则执行业务并落去重
4. 重复请求直接返回历史结果
5. 失败状态可按状态机补偿
记忆钩子:先定义问题,再说明一致性、可用性、分区、复制、时钟和故障模型的取舍;回答时要紧扣「幂等性」这道题,不要把相邻概念混成一段泛泛的分布式套话。
- 误区:幂等就是接口不报错。 幂等关注业务结果一致,不是简单吞异常。
- 误区:只有支付需要幂等。 下单、发券、扣库存、消息消费、任务调度都需要。
- 误区:前端防重复点击就够了。 网络重试、MQ 重投和服务端超时仍会造成重复请求。
- 追问:常见幂等方案有哪些? 唯一索引、去重表、业务状态机、版本号、Token 机制。
- 追问:幂等键怎么选? 选业务唯一请求号,如订单号、支付流水号、消息 ID。
- 追问:幂等和分布式锁有什么区别? 锁控制并发进入,幂等保证重复执行结果一致。
八、加强记忆
幂等就是让同一业务操作重复执行也只产生一次副作用。分布式里因为超时、重试、MQ 重投无法避免,所以写接口必须幂等。核心前提是稳定业务唯一标识,核心手段是唯一索引、去重表、Token、状态机、乐观锁和原子操作。记住两条底线:先查后写不可靠,分布式锁不等于幂等。