什么是幂等性(Indempotent)
幂等性(Indempotent)是数学中的一个概念。
对于接口而言,以相同的参数调用这个接口一次和多次时,对系统产生的影响是相同的,那么我们就认为这个接口是一个幂等接口。
为什么需要幂等性
并不是所有接口都需要保证幂等性。以相同的请求调用这个接口一次或多次,需要给调用方返回一致的结果时,就要考虑将这个接口设计成幂等接口。
什么情况下会产生接口幂等性问题
- 网络波动,可能会引起重复请求
- 用户重复操作,用户在操作时候可能会无意触发多次下单交易,甚至没有响应而有意触发多次交易应用
- 使用了失效或超时重试机制(Nginx重试、RPC重试或业务层重试等)
- 页面重复刷新
- 使用浏览器后退按钮重复之前的操作,导致重复提交表单
- 使用浏览器历史记录重复提交表单
- 浏览器重复的HTTP请求
- 定时任务重复执行
- 用户双击提交按钮
幂等的使用场景
- 微信领红包接口:对于一个红包,领一次和领多次具有一样的效果,故该场景下领红包接口需保证幂等性。
- 订单创建接口:在创建订单时,第一次调用返回超时了,重试机制一般会再次调用这个接口。此时我们不能因为这个接口被调了两次,就创建两个一样的订单;因此需保证幂等性。
- 库存扣减接口
-
如何保证幂等性
前端设计
从前端设计角度考虑,是通过防止重复提交来保证幂等性(但无法完全保证)。
防止重复提交更多的是不让用户发起多次一样的请求。比如用户在线购物下单时点了提交订单按钮,但是由于网络原因响应很慢,此时用户比较心急,多次点击了订单提交按钮。这种情况下就可能会造成多次下单。
一般防止重复提交的方案有将订单按钮置灰,跳转到结果页等。主要还是从客户端的角度来解决重复提交的问题。
幂等更多的是在重复请求已经发生,或是无法避免的情况下,采取一定的技术手段让这些重复请求不给系统带来副作用。1.按钮只能点击一次
2.页面重定向-PRG模式
PRG模式,即 Post-Redirect-Get,当客户提交表单后,去执行一个客户端的重定向,跳转到提交成功页面,避免用户按F5刷新导致的重复提交,也能消除按浏览器后退键导致的重复提交问题。目前绝大多数公司都是这么做的,如淘宝,京东等。
后端设计
1.来源+序列号
这是一种比较好理解的通用的方案。
当调用接口时,参数中必须传入来源 source 字段和序列号 seq 字段(实际项目中可用其他唯一标识字段代替,如序列号 uuid 等),服务端接收到请求,先判断自己是否是一个幂等接口,如果不是幂等接口就正常处理请求。
如果是一个幂等接口,就将 source 和 seq 组成联合主键,去数据库表或 Redis 中查询。如果没有查询到,说明没处理过这个请求,然后正常处理请求就行了。处理完之后将处理结果和 source 和 seq 信息一起存入数据库或 Redis 中。
如果根据 source 和 seq 能查询到,说明已经处理过这个请求了,直接将处理的结果返回即可。
这种方案非常简单,而且易于理解,比较通用。但是如果请求量很大的话,存放请求记录的表会很大,这个时候可以将一段时间之前的记录删除,以提升性能。2.唯一索引
此方案适合用于执行新增(create)操作的接口。
对业务唯一的字段加上唯一索引,这样当数据重复时,插入数据会抛出异常。
比如新增用户接口,我们将用户表中的身份证字段加上唯一索引。当同一个请求调用两次时,我们可以先 select 后 insert。先根据身份证字段查询下用户是否存在,不存在的话再新增。存在的话就返回新增失败,或者直接新增也行,数据库会抛异常,我们对异常处理返回前台就行了。
大家可能会有一个疑问,我同一个请求调用两次,第一返回新增成功,第二次返回失败,返回的结果不同啊。这个接口还是幂等接口吗?
此处,重申下概念,幂等强调的是接口一次调用和多次调用产生的效果是一样的。这边调用一次和调用多次都是新增了一个对象,所以还是满足幂等的。3. 乐观锁
此方案适用于执行更新(update)操作的接口。
乐观锁只是在更新数据那一刻锁表,其他时间不锁表,所以相对于悲观锁,效率更高。 我们一般通过数据库来实现乐观锁,比较通用的做法是增加一个时间戳(timestamp)或版本号(version)字段。
此处以版本号(version)为例进行说明。 先根据标识信息(如用户ID)查询用户信息,获取当前 version 信息
select id,amount,version from id=123;
如果数据存在,假设此次查询结果 version = 1,再使用 version 和 id 字段作为查询条件去执行更新操作
update user set amount = amount+100, version = version+1 -- version是第一步查询到的值,此处假设为1 where id=123 and version = 1;
- 根据 update 更新操作影响的行数是否大于0,来判断此次更新操作是否成功
第1次请求 version = 1 是可以成功的,操作成功后 version 变成 2 了。这时如果并发的请求过来,再执行相同的SQL
update user set amount = amount+100, version = version+1 where id=123 and version = 1;
该 update 操作不会真正更新数据,最终 SQL 执行结果影响行数是0。
4. 状态机控制
此方法适合在有状态机流转的情况下。
比如订单业务系统中,订单的状态有待支付,支付中,支付成功,支付失败。设计时最好只支持状态的单向改变,比如待支付一定在支付中的前面,这样在更新的时候就可以加上对当前订单状态的限定条件。例如想把订单状态更新为支付成功,则之前的订单状态必须为支付中。
代码实现中,对于状态字段,可以使用 int 类型,并且通过值的大小来做幂等。比如待支付订单为0,支付中为1,支付成功为100。在做状态机更新时,我们就可以这样控制
update `order` set status = #{status} where id = #{id} and status < #{status}
5. 分布式锁
执行方法时。现根据业务唯一ID获取分布式锁。若获取成功则执行;若获取锁不成功则不执行。分布式锁可以基于Redis,Zookeeper,MySQL来实现。
此处以Redis分布式锁在订单业务中的应用为例进行说明用户通过浏览器发起请求,服务端生成订单号 code 作为唯一业务字段。
- 使用 Redis 的 set 命令,将该订单 code 设置到 Redis 中,同时设置超时时间。
- 判断是否设置成功,如果设置成功,说明是第一次请求,则进行后续数据操作。
- 如果设置失败,说明是重复请求,则直接返回成功。
需要注意的是,分布式锁一定要设置一个合理的过期时间,如果设置过短,无法有效的防止重复请求。如果设置过长,可能会浪费 Redis 的存储空间,需要根据实际业务情况而定。
6.去重表
此方法适用于在业务中有唯一标识的插入场景中,比如在支付业务中,若一个订单只会支付一次,则订单ID可以作为唯一标识。
创建一张去重表,将业务唯一ID作为唯一索引,如订单号。当想针对订单做一系列操作时,先向去重表中插入一条记录,若插入成功,执行后续操作;若插入失败,数据库会抛出唯一约束异常,不执行后续操作。
去重表本质上可以看成基于 MySQL实现的分布式锁。
7.token机制
此种方案需要两次请求才能完成一次业务操作(增加了性能损耗和负载)
- 第一次请求获取token
- 第二次请求带着这个token,完成业务操作。
具体步骤如下
- 用户访问页面时,浏览器自动发起获取token请求。
- 服务端生成token,保存到redis中,然后返回给浏览器。
- 用户第2次通过浏览器发起请求时,携带该token。
- 在redis中查询该token是否存在,如果存在表示是第一次请求,做则后续的数据操作,并删除token。
- 如果不存在,说明是重复请求,则直接返回成功。
作者:变速风声
链接:https://juejin.cn/post/7098355055610298404
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。