肖sir__面试题__接口测试的幂等性测试
一)什么是接口幂等
概念:接口幂等性就是用户对于同一个接口发起的一次请求或者多次请求的结果是一致的,不会因为多次请求而产生不同的结果。
案例
用户购买商品后需要进行支付,支付扣款成功,但是返回结果的时侯报网络异常,此时钱已经扣了,用户不知道并再次点击支付按钮,此时会进行第二次扣款,返回结果成功,用户查询余额发现扣了两次款,多扣钱了,流水记录也生成了两条,这其实就没有保证接口的幂等性。
二)增删改查涉及的幂等性问题
查询操作
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】,发现这张表的序列号已经在第一次插入进去了,那么第二次操作就什么都不进行,
直接返回,保证幂等。
三、数据库锁
数据库悲观锁
指的就是每次操作的时候,先把记录锁定起来,其他人无法操作这条记录
select * from user where user_id = 1 for update;
注意:数据库悲观锁使用时,一般伴随事务一起使用,数据锁定时间可能会很长,需要根据实际情况选用。
案例: 当要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。
这里以更新商品订单状态来举例:一般订单有订单创建、订单确认、订单支付、订单完成、取消订单等订单流程。
当我更新订单状态为订单完成的时候,我们首先通过判断该订单的状态是否是订单支付,如果不是则直接返回,否则更新状态为已完成。
这是我们常见的一种写法,但这种写法在高并发环境下,可能会造成一个业务被执行两次的情况发生:
同时有两个请求过来,大家几乎同时查数据库订单状态,都是订单支付状态,然后就支持接下来一系列操作,这就导致一个业务被执行了两次,如果接下来一系列操作不是幂等的
那么就会出现脏数据。这里我们就可以通过悲观锁实现,也就是添加for update字段。
1)这里order_no需要添加索引,否则会锁表。
2)悲观锁在同一事务操作过程中,锁住了一行数据。悲观锁性能不佳所以一般不建议用悲观锁做这个事情。
数据库乐观锁
就是利用版本号的概念,在操作前先获取到操作记录的当前version版本号,然后操作的时候带上此版本号。
update user set age = age + 1, version = version + 1 where user_id = 2 and version = 1
注意:乐观锁主要使用于处理读多写少的问题。
案例:乐观锁就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制。
所谓的乐观锁就是在表中新增一个version(版本号)字段。
通过版本号的方式,来控制update的操作的幂等性,用户查询出要修改的数据,系统将数据返回给页面,将数据版本号放入隐藏域,用户修改数据,点击提交,将版本号一同提交给后台,后台使用版本号作为更新条件。
注意:乐观锁能够保证的是update的操作的幂等性,如果你的update本身就是幂等操作或者install操作那就不能用乐观锁了。
四、redis set防重
很多数据需要处理,只能被处理一次,比如我们可以计算数据的MD5
将其放入redis的set数据结构,每次处理数据,先看这个MD5是否已经存在,如果已经存在就不处理。
五、基于分布式锁
如果多个线程可能在同一时间处理相同的数据,比如多个线程在同一时刻都拿到了相同的数据处理,我们就可以加分布式锁,锁定此数据,处理完成后释放锁。获取到锁的必须先判断这个数据是否被处理过。
分布式锁实现幂等性的逻辑就是,请求过来时,先去尝试获得分布式锁,如果获得成功,就执行业务逻辑,反之获取失败的话,就舍弃请求直接返回成功。其实前面介绍过的悲观锁,本质是使用了数据库的分布式锁,都是将多个操作打包成一个原子操作,保证幂等。但由于数据库分布式锁的性能不太好,
我们可以改用:redis或zookeeper来实现分布式锁。
六、基于唯一索引
一般来讲悲观锁、乐观锁、状态码作用于update操作来实现幂等,而唯一索引是针对insert操作来保证幂等。
1) 创建订单时,前端先通过接口获取订单号,再请求后端时带入订单号,订单表中订单号添加唯一索引,如果存在插入相同订单号则直接报错。
2) 消费MQ消息时,messageId是唯一的,我们可以新添加一种消费记录表,将messageId作为主键,如果重复消费那么就会存在相同的messageId,插入直接报错。
具体流程步骤:
建立一张去重表,其中某个字段需要建立唯一索引
客户端去请求服务端,服务端会将这次请求的一些信息插入这张去重表中
因为表中某个字段带有唯一索引,如果插入成功,证明表中没有这次请求的信息,则执行后续的业务逻辑
如果插入失败,则代表已经执行过当前请求,直接返回。
七、基于token机制实现
token令牌机制应该是市面上用的比较多的一种保证幂等方式,简单理解
就是每次请求都拿着一张门票,这个门票是一次性的,用过一次就被毁掉了
不能重复利用。这个token令牌就相当于门票的概念,每次接口请求的时候带上token令牌
服务器第一次处理的时候去校验token,并且这个token只能用一次
如果用户使用相同的令牌请求二次,那么第二次就不处理,直接返回。
大致的流程
1、服务端提供了发送Token的接口,在执行业务前,先去获取Token,服务器会把Token保存到redis中;
2、然后调用业务接口请求时,作为请求参数或者请求头中传递;
3、服务器判断token是否存在redis中,存在表示第一次请求,然后删除token,继续执行业务;
4、服务器如果短时间内重复提交这个接口,因为两次请求token是一样的,所以第二次请求的时候,服务器校验token时,redis中已经没有了刚刚被第一次删掉的token,就表示是重复操作,所以第二次请求会校验失败,不作处理,这样就保证了业务代码,不被重复执行;
八、接口幂等性如何测试?
1、可以参考这篇帖子:https://developer.aliyun.com/article/1053461
2、可以参考这篇帖子:https://www.cnblogs.com/sea520/p/10117729.html
3、可以参考这篇帖子:https://www.cnblogs.com/jajian/p/10926681.html