1、幂等性基础:什么是幂等性、为什么重试需要幂等、幂等性的数学定义与工程定义
1.1 先聊聊我理解的幂等性
幂等性,说白了就是——同一个操作,执行一次和执行多次,结果是一样的。
举个例子。你去自动售货机买水,扫码支付了 5 块钱。机器出一次水,这叫正常。但如果网络卡了一下,你扫了两次码,扣了两次钱,机器却只出一瓶水——你肯定不干。反过来,如果扣了一次钱,机器出了两瓶水,商家也不干。
嗯,这里的关键就是:支付接口必须是幂等的。不管用户点了几次「确认支付」,最终只扣一次钱,只出一瓶水。
我在项目中遇到过最典型的场景,就是订单系统。用户下单时前端防重复提交做得再好,也防不住后端重试、MQ 消息重复消费、网关超时重发。你想想看,一个订单如果因为网络抖动被创建了两次,仓库就得发两件货,财务就得退一次款——全是线上事故。
幂等性的核心价值:让系统在面对重复请求时,表现得像只处理了一次。
1.2 为什么重试必须考虑幂等
重试是分布式系统里最常用的容错手段。服务调用超时了,重试;数据库写冲突了,重试;消息消费失败了,重试。但重试有个天然的问题——你没法区分「请求没到」和「请求到了但响应丢了」。
我举个例子你就明白了:
- 客户端调用支付接口,超时了。
- 客户端重试,再次调用支付接口。
- 实际上第一次调用已经成功了,只是响应在网络中丢了。
- 第二次调用如果没做幂等处理,就会重复扣款。
这就是重试带来的「重复执行」风险。所以,凡是可能被重试的接口,都必须设计成幂等的。这不是可选项,是必选项。
我曾经踩过的坑:早期做支付回调时,没考虑幂等,结果银行回调重试了三次,我们系统就发了三笔账。对账时发现多了两笔,差点背 P0 事故。从那以后,我所有写接口的第一条设计原则就是——先幂等,再业务。
1.3 数学定义 vs 工程定义
聊完直觉,咱们上点硬货。幂等性其实有严格的数学定义,也有更实用的工程定义。两者都得懂。
数学定义
在数学里,幂等性是这样定义的:
对于函数 f,如果 f(f(x)) = f(x) 对所有 x 成立,则 f 是幂等的。
什么意思?就是你把一个值丢进函数,得到结果;再把结果丢进去,结果不变。典型的例子是值函数:abs(abs(-5)) = abs(-5) = 5。
在计算机科学里,这个定义被扩展到了操作上:
对于操作 op,如果 op(op(state)) = op(state),则 op 是幂等的。
说白了,就是操作多次后的系统状态,和操作一次后的系统状态完全一致。
工程定义
工程上的定义更接地气:
一个接口,使用相同的请求参数调用多次,产生的副作用与调用一次相同。
注意这里的关键词是「副作用」。查询接口天然幂等,因为没副作用。但写接口就不一定了——创建订单、扣减库存、转账汇款,这些都有副作用。
我习惯把工程幂等分成两类:
| 类型 | 定义 | 例子 |
|---|---|---|
| 天然幂等 | 操作本身多次执行结果一致 | DELETE /users/123(删除一次和删除多次,结果都是用户不存在) |
| 强制幂等 | 通过业务逻辑保证多次执行结果一致 | 创建订单时用唯一流水号去重,重复请求直接返回已有结果 |
我个人习惯:所有写接口都按「强制幂等」来设计。天然幂等的情况太少,而且业务一变就可能不幂等了。与其事后补,不如一开始就上幂等方案。
1.4 一张图看懂幂等性核心逻辑
下面这张图,是我自己总结的幂等性判断流程。每次设计新接口时,我都会过一遍这个逻辑:
这个流程的核心就两点:先判断是不是天然幂等,如果不是,就上强制幂等方案。别偷懒,也别想当然。
1.5 幂等性设计的三个层次
我习惯把幂等性设计分成三个层次,从浅到深:
- 接口层:通过请求唯一标识(如幂等键、流水号)来去重。这是最常用的方式。
- 业务层:通过业务状态机来保证。比如订单状态从「待支付」到「已支付」只能流转一次。
- 存储层:通过数据库约束来保证。比如唯一索引、乐观锁、分布式锁。
这三个层次不是互斥的,而是互补的。我一般建议三层都做——接口层挡掉大部分重复请求,业务层兜住状态异常,存储层做最后一道防线。
一个小建议:设计幂等方案时,先想清楚「什么算重复」。是同一个用户、同一个流水号、还是同一个请求体?定义清楚了,方案才能落地。
1.6 本章小结
幂等性不是什么高深的概念,但它是分布式系统的基石之一。重试是刚需,幂等是重试的前提。没有幂等性的重试,就是在给线上埋雷。
记住三句话:
- 幂等 = 多次执行 = 一次结果
- 所有可能被重试的接口,都必须幂等
- 先判断天然幂等,不行就上强制方案