接口幂等性

Posted top啦它

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了接口幂等性相关的知识,希望对你有一定的参考价值。

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

一、什么是接口幂等

概念

接口幂等性就是用户对于同一个接口发起的一次请求或者多次请求的结果是一致的,
不会因为多次请求而产生不同的结果。

案例

用户购买商品后需要进行支付,支付扣款成功,但是返回结果的时侯报网络异常,
此时钱已经扣了,用户不知道并再次点击支付按钮,此时会进行第二次扣款,返回结果成功,
用户查询余额发现扣了两次款,多扣钱了,流水记录也生成了两条,这其实就没有保证接口的幂等性。

增删改查涉及的幂等性问题

查询操作

select * from user where user_id = 1;
不管执行多少次上面的查询语句,如果数据库记录没有变更的话,
查询结果都是一样的。由此可见,select是天然的幂等操作。

删除操作

delete from user where user_id = 1;
不管删除多少次,都是把数据删除,在不考虑返回结果的情况下,因此删除操作也是具有幂等性的。

更新操作

update user set age = age + 1 where user_id = 1;
将age字段进行递增操作,每执行一次,那么age就会加一,
显然每次执行结果都不一样,所以这种情况下update操作就不是幂等操作。

新增操作

insert into order(pkid,order,id,xx) values (1,'1231736721763762',''');
假设pkid是自增的,如果order_id没有做唯一约束的话,
那么可能导致同一个订单保存多条数据,不具备幂等性;如果order_id做了唯一约束,
那么这个新增就是幂等的。

哪些情况需要保证接口幂等?

对于业务中需要考虑幂等性的地方一般都是接口的重复请求,
重复请求是指同一个请求因为某些原因被多次提交。
导致这个情况会有几种场景。下面列举。

1、前端重复提交

用户在新增页面上快速点击多次,造成发了多次请求,后端重复保存了多条一摸一样的数据。
如用户提交订单,生成很多重复的订单。

2、消息重复消费

消息重复消费,一般是指消息中间件。如RabbitMQ,由于网络抖动,
MQ Broker将消息发送给消费端消费,消费端进行了消费,
在返回ack给MQ Broker时网络中断等原因,导致MQ Broker认为消费端没能正常消费,
这时候MQ Broker会重复将这条消息发送给进行消费,
如果没有做幂等,就会造成消费端重复消费同一条消息。

3、页面回退再次提交

举个例子,用户购买商品的时候,如果第一次点击下单按钮后,提示下单成功,
跳转到下单成功页面,这时候如果用户点击浏览器返回按钮,返回上一个下单页面。
重新点击下单按钮,这时候如果没有做幂等的话,也会造成重复下单的问题。

4、微服务互相调用

分布式系统中,服务之间的通信一般都通过RPC或者Feign进行调用,避免网络出小问题,
导致此次请求失败,这时这些远程调用,比如feign都会触发重试机制,
所以我们也需要保证接口幂等。

如何实现接口幂等

一、前端

如防止表单重复提交,按钮置灰、隐藏、按钮不可点击等方式。

二、后端

插入数据,应该按照唯一索引进行插入,比如订单号,相同的订单号就不可能有两条记录插入,
我们在数据库层面防止重复。这个机制是利用了数据库的主键唯一索引的特性,
解决了在插入场景时的幂等问题。
 建立了一个l唯一序列号是唯一索引,我们在进行业务操作的时候,往这张表插入一条数据,
 如果后面第二次提交【序列号还是一样,比如订单ID】,
 发现这张表的序列号已经在第一次插入进去了,那么第二次操作就什么都不进行,
 直接返回,保证幂等。

三、redis set防重

很多数据需要处理,只能被处理一次,比如我们可以计算数据的MD5
将其放入redis的set数据结构,每次处理数据,先看这个MD5是否已经存在,
如果已经存在就不处理。

四、数据库锁

数据库悲观锁

指的就是每次操作的时候,先把记录锁定起来,其他人无法操作这条记录
 select * from user where user_id = 1 for update;
 注意,数据库悲观锁使用时,一般伴随事务一起使用,数据锁定时间可能会很长,
 需要根据实际情况选用。

数据库乐观锁

就是利用版本号的概念,在操作前先获取到操作记录的当前version版本号,
然后操作的时候带上此版本号。
update user set age = age + 1, version = version + 1 where user_id = 2 and version = 1
注意,乐观锁主要使用于处理读多写少的问题。

五、业务层分布式锁

如果多个线程可能在同一时间处理相同的数据,
比如多个线程在同一时刻都拿到了相同的数据处理,我们就可以加分布式锁,
锁定此数据,处理完成后释放锁。获取到锁的必须先判断这个数据是否被处理过。

六、Token机制

token令牌机制应该是市面上用的比较多的一种保证幂等方式,简单理解,
就是每次请求都拿着一张门票,这个门票是一次性的,用过一次就被毁掉了,
不能重复利用。这个token令牌就相当于门票的概念,每次接口请求的时候带上token令牌,
服务器第一次处理的时候去校验token,并且这个token只能用一次,
如果用户使用相同的令牌请求二次,那么第二次就不处理,直接返回。
大致的流程
1、服务端提供了发送Token的接口,在执行业务前,先去获取Token,
服务器会把Token保存到redis中;
2、然后调用业务接口请求时,作为请求参数或者请求头中传递;
3、服务器判断token是否存在redis中,存在表示第一次请求,然后删除token,
继续执行业务;
4、服务器如果短时间内重复提交这个接口,因为两次请求token是一样的,
所以第二次请求的时候,服务器校验token时,redis中已经没有了刚刚被第一次删掉的token,就表示是重复操作,所以第二次请求会校验失败,不作处理,
这样就保证了业务代码,不被重复执行;

流程图

不过token这种方案有一定的危险性,其实就在于服务端我们到底该如何去验证令牌。

1、先删除token【先删除token令牌,再执行业务】还是后删除token【先执行业务,再删除token令牌】;

(a)、先删除可能导致,业务确实没有执行,重试还带上了之前的token,
由于防重设计导致,请求还是不能执行;
(b)、后删除token问题很大,可能导致,业务处理成功,但是服务闪断,
出现超时,没有删除token,别人继续重试,导致业务被执行两次;
(c)、我们最好设计为先删除token,如果业务调用失败,就重新获取token再次请求。

2、token获取,比较和删除必须保证原子性

(a)、redis.get(token)【获取】、token.equals()【比较】、redis.del(token)
【删除】、如果这三个操作不是原子的,可能导致高并发下,
多个线程都获取到同样的数据,判断都成功,继续业务并发执行;
 (b)、可以在redis中使用lua脚本完成这个操作,保证上述操作原子性。

以上是关于接口幂等性的主要内容,如果未能解决你的问题,请参考以下文章

如何保证接口的幂等性?常见的实现方案有哪些?

如何保证接口的幂等性?常见的实现方案有哪些?

如何保证接口的幂等性?常见的实现方案有哪些?

Spring Boot + Redis实战-利用自定义注解+分布式锁实现接口幂等性

接口服务中的幂等性设计和防重保证,详细分析幂等性设计几种实现方法

接口幂等性