什么是幂等性?
幂等性最早是数学里面的一个概念,后来被用于计算机领域。
接口的幂等性实际上就是接口可重复调用,在调用方多次调用的情况下,接口最终得到的结果是一致的。
什么情况下需要幂等性?
在微服务架构下或者调用外部接口时,我们在完成一个订单流程时经常遇到下面的场景:
- 一个订单创建接口,第一次调用超时了,然后调用方重试了一次
- 在订单创建时,我们需要去扣减库存,这时接口发生了超时,调用方重试了一次
- 当这笔订单开始支付,在支付请求发出之后,在服务端发生了扣钱操作,接口响应超时了,调用方重试了一次
- 一个订单状态更新接口,调用方连续发送了两个消息,一个是已创建,一个是已付款。但是你先接收到已付款,然后又接收到了已创建
- 在支付完成订单之后,需要发送一条短信,当一台机器接收到短信发送的消息之后,处理较慢。消息中间件又把消息投递给另外一台机器处理
为了解决以上问题,就需要保证接口的幂等性。
总的来说在增删改查4个操作中,尤为注意就是增加或者修改
查询操作
查询对于结果是不会有改变的,查询一次和查询多次,在数据不变的情况下,查询结果是一样的。select是天然的幂等操作
删除操作
删除一次和多次删除都是把数据删除。(多次操作,结果一样,具备幂等性。注意删除的数据不存在时,可能返回结果不一样)
更新操作
修改在大多场景下结果一样,但是如果是增量修改是需要保证幂等性的,如下例子:
把表中 id 为 XXX 的记录的A字段值设置为1,这种操作不管执行多少次都是幂等的
把表中 id 为 XXX 的记录的A字段值增加1,这种操作就不是幂等的
新增操作
增加在重复提交的场景下会出现幂等性问题,如以上的支付问题
如何保证幂等性
token机制
1、服务端提供了发送token的接口。我们在分析业务的时候,哪些业务是存在幂等问题的,就必须在执行业务前,先去获取token,服务器会把token保存到redis中。
2、然后调用业务接口请求时,把token携带过去,一般放在请求头部。
3、服务器判断token是否存在redis中,存在表示第一次请求,然后删除token,继续执行业务。
4、如果判断token不存在redis中,就表示是重复操作,直接返回重复标记给client,这样就保证了业务代码,不被重复执行。
关键点 先删除token,还是后删除token。
后删除token:如果进行业务处理成功后,删除redis中的token失败了,这样就导致了有可能会发生重复请求,因为token没有被删除。这个问题其实是数据库和缓存redis数据不一致问题。
先删除token:如果系统出现问题导致业务处理出现异常,业务处理没有成功,接口调用方也没有获取到明确的结果,然后进行重试,但token已经删除掉了,服务端判断token不存在,认为是重复请求,就直接返回了,无法进行业务处理了。
先删除token可以保证不会因为重复请求,业务数据出现问题。出现业务异常,可以让调用方配合处理一下,重新获取新的token,再次由业务调用方发起重试请求就ok了。
token机制缺点
业务请求每次请求,都会有额外的请求(一次获取token请求、判断token是否存在的业务)。其实真实的生产环境中,1万请求也许只会存在10个左右的请求会发生重试,为了这10个请求,我们让9990个请求都发生了额外的请求。
唯一索引
创建唯一性索引的表来实现幂等性,在每次执行业务之前,先执行插入操作,因为唯一字段就是主键ID,因此如果重复插入的话会触发唯一约束而导致插入失败,在这种情况下(插入失败我们就可以判断他为重复提交的请求)
如果是分库分表场景下,路由规则要保证相同请求下,落在同一个数据库和同一表中,要不然数据库主键约束就不起效果了,因为是不同的数据库和表主键是不相关的。
唯一ID
调用接口时,生成一个唯一id,redis将数据保存到集合中(去重),存在即处理过。
通过悲观锁来实现幂等性
首先来判断数据的状态:
select status from table_name where id='xxx';
然后再进行操作:
insert into table_name(id) values('xxx');
update table_name set status = 'xxx';
这种情况是非原子操作,所以在高并发环境下可能会造成一个业务被执行两次的问题。
当一个程序在执行中时,而另一个程序也开始状态判断的操作。
所以需要使用事务
开启事务
select status from table_name where id='xxx';
insert into table_name(id) values('xxx');
update table_name set status = 'xxx';
提交事务
注意:MySQL数据库必须选用 innodb 存储引擎,且 id 字段一定要是主键或者是唯一索引,不然会锁表,影响其他业务执行
分布式锁实现
分布式锁实现幂等性的逻辑是,在每次执行方法之前判断,是否可以获取到分布式锁,如果可以,则表示为第一次执行方法,否则直接舍弃请求即可。
需要注意的是分布式锁的key必须为业务的唯一标识,通常适用redis或者zookeeper来实现分布式锁。