彻底理解接口幂等性

目录

背景

1. 幂等性的概念

2.什么情况需要处理接口幂等性问题?

2.1 select 天然自带幂等性。

2.2 insert 当我们重复插入数据的时候,会出现什么情况 ?

2.3 delete 是否具有幂等性?

2.4 update 猜一猜是否具有天热幂等性?

3. 接口幂等性解决方案

3.1 唯一索引,防止新增脏数据

3.2 token+redis机制

3.3 CAS 保证接口幂等性

3.4 悲观锁

3.5 乐观锁实现幂等

3.6 分布式锁

3.7 防重表

3.8 缓存队列


背景

微服务架构下,我们在完成一个订单流程时经常遇到下面的场景:

  • 一个订单创建接口,第一次调用超时了,然后调用方重试了一次
  • 在订单创建时,我们需要去扣减库存,这时接口发生了超时,调用方重试了一次
  • 当这笔订单开始支付,在支付请求发出之后,在服务端发生了扣钱操作,接口响应超时了,调用方重试了一次
  • 一个订单状态更新接口,调用方连续发送了两个消息,一个是已创建,一个是已付款。但是你先接收到已付款,然后又接收到了已创建
  • 在支付完成订单之后,需要发送一条短信,当一台机器接收到短信发送的消息之后,处理较慢。消息中间件又把消息投递给另外一台机器处理
  • 前端重复提交选中的数据,应该后台只产生对应这个数据的一个反应结果
  •  我们发起一笔付款请求,应该只扣用户账户一次钱,当遇到网络重发或系统bug重发,也应该只扣一次钱
  • 发送消息,也应该只发一次,同样的短信发给用户,用户会哭的
  • 创建业务订单,一次业务请求只能创建一个,创建多个就会出大问题等等

以上问题,就是在单体架构转成微服务架构之后,带来的问题。当然不是说单体架构下没有这些问题,在单体架构下同样要避免重复请求。但是出现的问题要比这少得多。

1. 幂等性的概念

幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。 在编程中.一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。例如,“getUsername()和setTrue()”函数就是一个幂等函数。更复杂的操作幂等保证是利用唯一交易号(流水号)实现,综上所述:幂等就是一个操作,不论执行多少次,产生的效果和返回的结果都是一样的。 

为了解决以上问题,就需要保证接口的幂等性,接口的幂等性实际上就是接口可重复调用,在调用方多次调用的情况下,接口最终得到的结果是一致的。有些接口可以天然的实现幂等性,比如查询接口,对于查询来说,你查询一次和两次,对于系统来说,没有任何影响,查出的结果也是一样。

接口幂等性就是用户对同一操作发起了一次或多次请求的对数据的影响是一致不变的,不会因为多次的请求而产生副作用。

副作用:可以认为多次请求操作,每一次对数据状态都会产生影响 。
注意这里并没有要求接口返回结果是一致的。

例如:update order set moeny = 100 where orderId = 2029282312
该操作无论执行多少次,对数据的影响都是一致的,不变的。

2.什么情况需要处理接口幂等性问题?

2.1 select 天然自带幂等性。

每次查询对数据都不会产生副作用。

2.2 insert 当我们重复插入数据的时候,会出现什么情况 ?

第一种情况:自增主键,没有幂等性。

eg:insert into product_info (id,name,type,price,tm)
执行多次,会新增多条记录。对结果集产生了副作用。

第二种情况:业务主键,具有幂等。

eg:insert into product_info (orderId,name,type,price,tm) orderId 为主键唯一
无论该sql执行多少次,对结果集产生的效果都是一样只增加了一条数据。

2.3 delete 是否具有幂等性?

第一种情况:绝对删除,具有幂等性。

eg;delete from order where id = 3 。
无论该sql执行多少次,对结果集产生的效果都是一样只删除了一条数据。

第二种情况: 相对删除,不具有幂等性。

eg:delete from order where id > 23 .
该操作每执行一次,对结果集产生的结果,可能都不一样,同一操作多次执行对数据产生了副作用。

2.4 update 猜一猜是否具有天热幂等性?

第一种情况:绝对更新,具有幂等性。

eg:update good set stock= 586 where goodId = 10;
该操作无论执行多少次操作对结果的影响都是一样。

第二种情况:相对更新,不具有幂等性。

eg:update good set stock = stock+10 where goodid= 10 ;
每次执行该操作库存数量都会加10,所以不具备幂等操作。

总结:以上都是基于单库,单表的操作幂等性的分析,其实在具体业务当中,可能要设计多个表,多个库,甚至跨服务操作。比如分布式系统中,我们一个接口,可能需要调用多个服务来完成任务。那么这种情况,如何保证接口的幂等性呢?

3. 接口幂等性解决方案

接口幂等处理要根据具体业务来判断怎么处理,以下会举例来阐述接口幂等处理解决方案。

3.1 唯一索引,防止新增脏数据

比如:支付宝的资金账户,支付宝也有用户账户,每个用户只能有一个资金账户,怎么防止给用户创建资金账户多个,那么给资金账户表中的用户ID加唯一索引,所以一个用户新增成功一个资金账户记录。要点:唯一索引或唯一组合索引来防止新增数据存在脏数据(当表存在唯一索引,并发时新增报错时,再查询一次就可以了,数据应该已经存在了,返回结果即可)。

3.2 token+redis机制

比如订单支付场景:
        该支付分为两个步骤:
                1.1 获取全局唯一token
                       接口处理生成唯一标识(token) 存储到redis中(redis单线程的,处理需要排队),并返回给调用客户端。
                1.2 发起支付操作并附带token
                    接口处理:
                    1.2.1 获得分布式锁(处理并发情况)
                    1.2.2 判断redis中是否存在token
                    1.2.3 存在 执行支付业务逻辑,否则返回该订单已经支付
                    1.2.4 释放分布式锁

思考:为什么要加分布式锁?
原因1:在高并发请求中 ,token判断是否存在是非线程安全的,所以要加分布式锁来保证 该条件的判断为线程安全

注释:也可用redis删除操作来判断token,删除成功代表token校验通过 这个删除是原子操作的

原因2:在支付业务中,判断支付订单是否已经存在,存在说明该订单已经支付过了,不存在就执行扣款操作,如果相同操作并发两个请求来到判断条件可能两个请求都能判断支付订单不存在,造成重复扣款。 所以也要加分布式锁保证线程的安全。

token特点:要申请,一次有效性,可以限流。注意:redis要用删除操作来判断token,删除成功代表token校验通过,如果用select+delete来校验token,存在并发问题,不建议使用;

3.3 CAS 保证接口幂等性

状态机制幂等(状态不可逆)
针对更新操作:
例如 电商订单,订单支付状态 0 待支付 , 1 支付中 , 3 支付成功 4 支付失败。

update order set status = 1 where status =0 and orderId = “201251487987”
该sql语句利用状态CAS 保证该操作的幂等。

        eg:比如要进行订单支付,上来先用CAS更新订单状态,
            返回影响说为1 代表修改成功,可以支付,继续执行支付业务代码
            返回影响数 0 代表修改失败,该订单已经不是待支付订单了。

注释:实际这里是利用CAS原理

CAS基础实现
CAS 是 compare and swap 的简写,即比较并交换。它是指一种操作机制,而不是某个具体的类或方法。在 Java 平台上对这种操作进行了包装。在 Unsafe 类中,调用代码如下:

unsafe.compareAndSwapInt(this, valueOffset, expect, update);
 它需要三个参数,分别是内存位置 V,旧的预期值 A 和新的值 B。

  1. 操作时,先从内存位置读取到值,然后和预期值A比较。
  2. 如果相等,则将此内存位置的值改为新值 B,返回 true。
  3. 如果不相等,说明和其他线程冲突了,则不做任何改变,返回 false。

这种机制在不阻塞其他线程的情况下避免了并发冲突,比独占锁的性能高很多。 CAS 在 Java 的原子类和并发包中有大量使用。

CAS底层实现 
CAS 底层是靠调用 CPU 指令集的 cmpxchg 完成的,它是 x86 和 Intel 架构中的 compare and exchange 指令。在多核的情况下,这个指令也不能保证原子性,需要在前面加上 lock 指令。

lock 指令可以保证一个 CPU 核心在操作期间独占一片内存区域。那么 这又是如何实现的呢?

在处理器中,一般有两种方式来实现上述效果:总线锁和缓存锁。在多核处理器的结构中,CPU 核心并不能直接访问内存,而是统一通过一条总线访问。

  • 总线锁就是锁住这条总线,使其他核心无法访问内存。这种方式代价太大了,会导致其他核心停止工作。
  • 缓存锁并不锁定总线,只是锁定某部分内存区域。当一个 CPU 核心将内存区域的数据读取到自己的缓存区后,它会锁定缓存对应的内存区域。锁住期间,其他核心无法操作这块内存区域。

CAS 就是通过这种方式实现比较和交换操作的原子性的。值得注意的是, CAS 只是保证了操作的原子性,并不保证变量的可见性,因此变量需要加上 volatile 关键字

CAS缺点
CAS虽然高效地解决了原子操作,但是还是存在一些缺陷的,主要表现在三个方面:

1.自旋时间太长

如果CAS一直不成功呢?这种情况绝对有可能发生,如果自旋CAS长时间地不成功,则会给CPU带来非常大的开销。在JUC中有些地方就限制了CAS自旋的次数,例如BlockingQueue的SynchronousQueue。

2.只能保证一个共享变量原子操作

看了CAS的实现就知道这只能针对一个共享变量,如果是多个共享变量就只能使用锁了,当然如果你有办法把多个变量整成一个变量,利用CAS也不错。例如读写锁中state的高低位。(https://www.cnblogs.com/wait-pigblog/p/9350569.html)

3.ABA问题

CAS需要检查操作值有没有发生改变,如果没有发生改变则更新。但是存在这样一种情况:如果一个值原来是A,变成了B,然后又变成了A,那么在CAS检查的时候会发现没有改变,但是实质上它已经发生了改变,只是又回到了原来的值而已,这就是所谓的ABA问题。对于ABA问题其解决方案是加上版本号,即在每个变量都加上一个版本号,每次改变时加1,即A —> B —> A,变成1A —> 2B —> 3A,采用AtomicStampedRdference类可以实现这个方案。

搞定CAS的原理,看这一篇就够了

3.4 悲观锁

悲观锁——获取数据的时候加锁获取。select * from table_xxx where id='xxx' for update; 注意:id字段一定是主键或者唯一索引,不然是锁表,会死人的,悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,根据实际情况选用。

3.5 乐观锁实现幂等

背景由来:

            为什么要有幂等这种场景?因为在大的系统中,都是分布式部署,如:订单业务 和 库存业务有可能都是独立部署的,都是单独的服务。用户下订单,会调用到订单服务和库存服务。

比如:订单系统:
订单服务 —> 库存服务 (PRC远程调用(服务接口))

        因为分布式部署,很有可能在调用库存服务时,因为网络等原因,订单服务调用失败,但其实库存服务已经处理完成,只是返回给订单服务处理结果时出现了异常。这个时候一般系统会作补偿方案,也就是订单服务再次发起库存服务的调用,库存减1

update t_goods set count = count -1 where good_id=2

          这样就出现了问题,其实上一次调用已经减了1,只是订单服务没有收到处理结果。现在又调用一次,又要减1,这样就不符合业务了,多扣了。

        幂等这个概念就是,不管库存服务在相同条件下调用几次,处理结果都一样。这样才能保证补偿方案的可行性。

乐观锁方案
    借鉴数据库的乐观锁机制,如:

 update t_goods set count = count -1 , version = version + 1 where good_id=2 and version = 1

乐观锁只是在更新数据那一刻锁表,其他时间不锁表,所以相对于悲观锁,效率更高。乐观锁的实现方式多种多样可以通过version或者其他状态条件:1. 通过版本号实现update table_xxx set name=#name#,version=version+1 where version=#version#如下图(来自网上);2. 通过条件限制 update table_xxx set avai_amount=avai_amount-#subAmount# where avai_amount-#subAmount# >= 0要求:quality-#subQuality# >= ,这个情景适合不用版本号,只更新是做数据安全校验,适合库存模型,扣份额和回滚份额,性能更高;


注意:乐观锁的更新操作,最好用主键或者唯一索引来更新,这样是行锁,否则更新时会锁表,上面两个sql改成下面的两个更好: 

update table_xxx set name=#name#,version=version+1 where id=#id# and version=#version#;
update table_xxx set avai_amount=avai_amount-#subAmount# where id=#id# and avai_amount-#subAmount# >= 0;

3.6 分布式锁

还是拿插入数据的例子,如果是分布是系统,构建全局唯一索引比较困难,例如唯一性的字段没法确定,这时候可以引入分布式锁,通过第三方的系统(redis或zookeeper),在业务系统插入数据或者更新数据,获取分布式锁,然后做操作,之后释放锁,这样其实是把多线程并发的锁的思路,引入多多个系统,也就是分布式系统中得解决思路。要点:某个长流程处理过程要求不能并发执行,可以在流程执行之前根据某个标志(用户ID+后缀等)获取分布式锁,其他流程执行时获取锁就会失败,也就是同一时间该流程只能有一个能执行成功,执行完成后,释放分布式锁(分布式锁要第三方系统提供)。

3.7 防重表

1.利用数据库建一张防重表(加唯一索引)

     比如订单支付,
        反正重复支付:订单号插入防重表 成功 执行支付业务逻辑,失败说明已经支付过。

防重表支付成功是否要删除:
1.可定期清除数据
2.也可结合 订单状态 ,在支付前查询订单状态为待支付 执行支付操作 ,操作后删除订单号 若 第二个请求插入防重表成功,但是这个时候查询订单状态失败。
(实际这个防重表就是实现了分布式锁)

3.8 缓存队列

将请求放入队列,后续使用异步任务处理队列中的数据,过滤掉重复的消息。 和防止重复消费道理是一样。

 有兴趣可以关注我的微信公众号“自动化测试全栈”,微信号:QAlife,学习更多自动化测试技术。

也可加入我们的自动化测试技术交流群,QQ群号码:301079813

主要探讨loadrunner/JMeter测试、Selenium/RobotFramework/Appium自动化测试、接口自动化测试,测试工具等测试技术,让我们来这里分享经验、交流技术、结交朋友、拓展视野、一起奋斗!

参考文章:

1.接口幂等性详解_完美天空的博客-CSDN博客_接口幂等性

 2.高并发下接口幂等性解决方案_Gandoph的博客-CSDN博客_接口幂等性解决方案

3.如何保证微服务接口的幂等性_wangyan9110's Blog-CSDN博客_如何保证幂等性

  • 2
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
随着互联网的发展和普及,网站和应用程序越来越多,用户也越来越多。对于这些网站和应用程序来说,如何记录用户的信息和状态是一个非常重要的问题,因为只有这样才能提供更好的服务和更好的用户体验。而cookie、session、token就是三种常用的记录用户信息和状态的方式。 首先,cookie是一种存储在客户端浏览器中的文本文件,用于存储用户的一些信息。当用户在浏览器中打开一个网站时,网站服务器会发送一个包含一些信息的cookie给用户的浏览器。浏览器在接收到cookie后会以键值对的形式将它们存储在本地,然后在下一次访问该网站时再将cookie发送给服务器。服务器通过读取这些cookie中的信息,就可以知道该用户的一些状态和偏好,从而提供更好的服务。 其次,session是一种服务器端技术,用于记录用户的会话状态。当用户第一次访问一个网站时,网站服务器会为该用户创建一个session,然后在服务器端存储用户的一些信息。当用户进行一些操作时,服务器端会根据该用户的session来判断其当前的状态和权限,并根据这些信息进行相应的处理。当用户关闭浏览器时,与该用户相关的session会被销毁。 最后,token是一种用于验证用户身份的令牌。当用户输入用户名和密码进行登录时,服务器会生成一个token,并将该token返回给客户端。客户端在后续的请求中需要带上该token,服务器收到请求后会根据token来验证用户身份,从而决定是否允许该请求。token不存储在客户端,而是存储在服务器端的数据库或者内存中,因此可以防止一些与cookie相关的安全问题,例如CSRF攻击和 XSS攻击。 总的来说,cookie、session、token可以作为不同的方式来记录用户的信息和状态,这些方式都有自己的优缺点,应该根据具体的需求来选择适合的方式。同时,为了确保安全,应该采取一些措施来减少一些可能出现的安全问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

慕城南风

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值