什么是幂等:把不确定的重试,变成确定的重复
幂等是指同一个操作,执行一次和执行很多次,对系统状态造成的最终结果是一样的。围绕这个概念,需要讲清三件事:它在解决什么问题,哪些操作天生幂等、哪些天生不幂等,以及真实系统里怎么把不幂等的操作改造成幂等。

什么是幂等
关键在于"最终"两个字。重复执行的过程中,系统可能被碰了很多次,中间状态也可能短暂变化,但只要请求停下来,落到的结果和只做一次是一样的,那它就满足幂等。

生活场景里的幂等与不幂等
电梯的呼叫按钮,你按一次,灯亮了;再按十次,灯还是亮着,电梯也只来一趟,这个按钮就是幂等的。而往投币口投一块钱就不幂等,投一次多一块,投十次多十块,次数直接改变结果。

代码层面的对照
把这个差别搬到代码里就很清楚。把变量赋值为 5 是幂等的,执行几遍它都是 5。而在当前值上加 1 不幂等,跑三遍就加了三。
同样,把订单状态改成已支付通常是幂等的,给余额增加一百元就不是。

它在解决什么问题:不可靠的网络
网络不可靠是这件事重要的原因。客户端把请求发出去,服务端其实处理成功了,但响应在回来的路上丢了,或者直接超时了。
这时客户端只知道没拿到结果,它唯一能做的就是重试。如果这个请求不幂等,一次超时就可能变成两笔扣款、两条记录、两次发货。

HTTP 协议层面的约定
HTTP 协议层面其实早就给过约定。GET、PUT、DELETE 被定义为幂等,POST 不是。

要注意,幂等说的是结果一致,不是说响应内容完全一样,也不是说不修改数据。删除一个资源,第一次删掉了,第二次返回不存在,但最终状态都是没有这个资源,所以它算幂等。

实践场景:把不幂等改造成幂等
第一招:幂等键(去重键)
真实系统里最常用的一招是幂等键,也叫去重键。每次请求都带上一个全局唯一的编号,比如订单号,或者随机生成的 UUID。
服务端收到后先查这个编号有没有处理过,处理过就直接把上次的结果返回,没处理过才真正执行,同时把编号和处理结果一起记录下来。这样重试就退化成了一次查历史记录,代价很小。

这里有个容易踩的坑,幂等键必须由发起方生成,不能在服务端生成。因为服务端每收到一次请求就会生成一个新编号,重试根本识别不出来。
另外这条记录要设有效期,比如保留二十四小时,不能无限堆下去,否则去重表会越来越大。

第二招:唯一约束
数据库给业务字段加唯一索引,比如每个用户每天只能领一张券。重复请求第二次插入时会被数据库直接拦下来,报唯一键冲突,业务层捕获这个异常,当作成功返回就行。

好处是不依赖应用层判断,并发再高也不会漏。
第三招:状态机加条件更新
更新时把当前状态写进条件里,比如只更新那些状态仍然是待支付的订单。这条语句第一次执行影响一行,第二次执行影响零行。
两个请求同时到达也一样,只有一个能把状态改走,另一个拿到零行。代码只看影响行数,就知道自己是不是真正生效的那一个请求。

三招叠着用
这三招在实际系统里通常不是三选一,而是叠着用。幂等键挡在入口,唯一约束让数据库兜底,条件更新保证只有一个请求真正改变状态,任何一层漏掉,后面还有机会拦住。

常见误区
还有一个常见误解,觉得只有写操作才需要幂等。其实读操作也可能有副作用,比如查询的同时顺带记一次点击、顺手扣一次额度。
判断标准很简单:只要一次调用会改变状态,就要想清楚它被重复调用会怎样。

常见问题
- 幂等是不是要求响应内容完全一样? 不是。幂等说的是结果一致,不是说响应内容完全一样,也不是说不修改数据。删除资源时第二次返回不存在,但最终状态都是没有这个资源,所以它算幂等。
- 幂等键可以由服务端生成吗? 不行。服务端每收到一次请求就会生成一个新编号,重试根本识别不出来,幂等键必须由发起方生成。
- 去重记录可以一直保留吗? 不能无限堆下去,否则去重表会越来越大,这条记录要设有效期,比如保留二十四小时。
- 只有写操作才需要幂等吗? 不是。读操作也可能有副作用,比如查询的同时顺带记一次点击、顺手扣一次额度。
总结
幂等的本质是让重复执行变得安全,它把不确定的重试,变成确定的重复。
判断一个接口要不要幂等,就看它被多调一次会不会多出一份副作用。
实现路径无非三条:用幂等键挡住重复请求,用唯一约束让数据库兜底,用条件更新保证只有一次真正生效。


