苏三说技术

「苏三说技术」 维护者目前就任于某出名互联网公司,从事开发、架构和局部管理工作。实战经验丰盛,对jdk、spring、springboot、springcloud、mybatis等开源框架源码有肯定钻研,欢送关注,和我一起交换。

29篇原创内容

公众号

前言


接口幂等性问题,对于开发人员来说,是一个跟语言无关的公共问题。本文分享了一些解决这类问题十分实用的方法,绝大部分内容我在我的项目中实际过的,给有须要的小伙伴一个参考。

不晓得你有没有遇到过这些场景:

  1. 有时咱们在填写某些form表单时,保留按钮不小心疾速点了两次,表中居然产生了两条反复的数据,只是id不一样。
  2. 咱们在我的项目中为了解决接口超时问题,通常会引入了重试机制。第一次申请接口超时了,申请方没能及时获取返回后果(此时有可能曾经胜利了),为了防止返回谬误的后果(这种状况不可能间接返回失败吧?),于是会对该申请重试几次,这样也会产生反复的数据。
  3. mq消费者在读取音讯时,有时候会读取到反复音讯(至于什么起因这里先不说,有趣味的小伙伴,能够找我私聊),如果解决不好,也会产生反复的数据。

没错,这些都是幂等性问题。

接口幂等性是指用户对于同一操作发动的一次申请或者屡次申请的后果是统一的,不会因为屡次点击而产生了副作用。

这类问题多发于接口的:

  • insert操作,这种状况下屡次申请,可能会产生反复数据。
  • update操作,如果只是单纯的更新数据,比方:update user set status=1 where id=1,是没有问题的。如果还有计算,比方:update user set status=status+1 where id=1,这种状况下屡次申请,可能会导致数据谬误。

那么咱们要如何保障接口幂等性?本文将会通知你答案。

1. insert前先select

通常状况下,在保留数据的接口中,咱们为了避免产生反复数据,个别会在insert前,先依据namecode字段select一下数据。如果该数据已存在,则执行update操作,如果不存在,才执行  insert操作。

该计划可能是咱们平时在避免产生反复数据时,应用最多的计划。然而该计划不适用于并发场景,在并发场景中,要配合其余计划一起应用,否则同样会产生反复数据。我在这里提一下,是为了防止大家踩坑。

2. 加乐观锁

在领取场景中,用户A的账号余额有150元,想转出100元,失常状况下用户A的余额只剩50元。个别状况下,sql是这样的:

update user amount = amount-100 where id=123;

如果呈现屡次雷同的申请,可能会导致用户A的余额变成正数。这种状况,用户A来可能要哭了。于此同时,零碎开发人员可能也要哭了,因为这是很重大的零碎bug。

为了解决这个问题,能够加乐观锁,将用户A的那行数据锁住,在同一时刻只容许一个申请取得锁,更新数据,其余的申请则期待。

通常状况下通过如下sql锁住单行数据:

select * from user id=123 for update;

具体流程如下:

具体步骤:

  1. 多个申请同时依据id查问用户信息。
  2. 判断余额是否有余100,如果余额有余,则间接返回余额有余。
  3. 如果余额短缺,则通过for update再次查问用户信息,并且尝试获取锁。
  4. 只有第一个申请能获取到行锁,其余没有获取锁的申请,则期待下一次获取锁的机会。
  5. 第一个申请获取到锁之后,判断余额是否有余100,如果余额足够,则进行update操作。
  6. 如果余额有余,阐明是反复申请,则间接返回胜利。
须要特地留神的是:如果应用的是mysql数据库,存储引擎必须用innodb,因为它才反对事务。此外,这里id字段肯定要是主键或者惟一索引,不然会锁住整张表。

乐观锁须要在同一个事务操作过程中锁住一行数据,如果事务耗时比拟长,会造成大量的申请期待,影响接口性能。

此外,每次申请接口很难保障都有雷同的返回值,所以不适宜幂等性设计场景,然而在防重场景中是能够的应用的。

在这里顺便说一下,防重设计幂等设计,其实是有区别的。防重设计次要为了防止产生反复数据,对接口返回没有太多要求。而幂等设计除了防止产生反复数据之外,还要求每次申请都返回一样的后果。

3. 加乐观锁

既然乐观锁有性能问题,为了晋升接口性能,咱们能够应用乐观锁。须要在表中减少一个timestamp或者version字段,这里以version字段为例。

在更新数据之前先查问一下数据:

select id,amount,version from user id=123;

如果数据存在,假如查到的version等于1,再应用idversion字段作为查问条件更新数据:

update user set amount=amount+100,version=version+1where id=123 and version=1;

更新数据的同时version+1,而后判断本次update操作的影响行数,如果大于0,则阐明本次更新胜利,如果等于0,则阐明本次更新没有让数据变更。

因为第一次申请version等于1是能够胜利的,操作胜利后version变成2了。这时如果并发的申请过去,再执行雷同的sql:

 update user set amount=amount+100,version=version+1where id=123 and version=1;

update操作不会真正更新数据,最终sql的执行后果影响行数是0,因为version曾经变成2了,where中的version=1必定无奈满足条件。但为了保障接口幂等性,接口能够间接返回胜利,因为version值曾经批改了,那么后面必然曾经胜利过一次,前面都是反复的申请。

具体流程如下:

具体步骤:

  1. 先依据id查问用户信息,蕴含version字段
  2. 依据id和version字段值作为where条件的参数,更新用户信息,同时version+1
  3. 判断操作影响行数,如果影响1行,则阐明是一次申请,能够做其余数据操作。
  4. 如果影响0行,阐明是反复申请,则间接返回胜利。

4. 加惟一索引

绝大数状况下,为了避免反复数据的产生,咱们都会在表中加惟一索引,这是一个非常简单,并且无效的计划。

alter table `order` add UNIQUE KEY `un_code` (`code`);

加了惟一索引之后,第一次申请数据能够插入胜利。但前面的雷同申请,插入数据时会报Duplicate entry '002' for key 'order.un_code异样,示意惟一索引有抵触。

虽说抛异样对数据来说没有影响,不会造成谬误数据。然而为了保障接口幂等性,咱们须要对该异样进行捕捉,而后返回胜利。

如果是java程序须要捕捉:DuplicateKeyException异样,如果应用了spring框架还须要捕捉:MySQLIntegrityConstraintViolationException异样。

具体流程图如下:

具体步骤:

  1. 用户通过浏览器发动申请,服务端收集数据。
  2. 将该数据插入mysql
  3. 判断是否执行胜利,如果胜利,则操作其余数据(可能还有其余的业务逻辑)。
  4. 如果执行失败,捕捉惟一索引抵触异样,间接返回胜利。

5. 建防重表

有时候表中并非所有的场景都不容许产生反复的数据,只有某些特定场景才不容许。这时候,间接在表中加惟一索引,显然是不太适合的。

针对这种状况,咱们能够通过建防重表来解决问题。

该表能够只蕴含两个字段:id惟一索引,惟一索引能够是多个字段比方:name、code等组合起来的惟一标识,例如:susan\_0001。

具体流程图如下:

具体步骤:

  1. 用户通过浏览器发动申请,服务端收集数据。
  2. 将该数据插入mysql防重表
  3. 判断是否执行胜利,如果胜利,则做mysql其余的数据操作(可能还有其余的业务逻辑)。
  4. 如果执行失败,捕捉惟一索引抵触异样,间接返回胜利。
须要特地留神的是:防重表和业务表必须在同一个数据库中,并且操作要在同一个事务中。

6. 依据状态机

很多时候业务表是有状态的,比方订单表中有:1-下单、2-已领取、3-实现、4-撤销等状态。如果这些状态的值是有法则的,依照业务节点正好是从小到大,咱们就能通过它来保障接口的幂等性。

如果id=123的订单状态是已领取,当初要变成实现状态。

update `order` set status=3 where id=123 and status=2;

第一次申请时,该订单的状态是已领取,值是2,所以该update语句能够失常更新数据,sql执行后果的影响行数是1,订单状态变成了3

前面有雷同的申请过去,再执行雷同的sql时,因为订单状态变成了3,再用status=2作为条件,无奈查问出须要更新的数据,所以最终sql执行后果的影响行数是0,即不会真正的更新数据。但为了保障接口幂等性,影响行数是0时,接口也能够间接返回胜利。

具体流程图如下:

具体步骤:

  1. 用户通过浏览器发动申请,服务端收集数据。
  2. 依据id和以后状态作为条件,更新成下一个状态
  3. 判断操作影响行数,如果影响了1行,阐明以后操作胜利,能够进行其余数据操作。
  4. 如果影响了0行,阐明是反复申请,间接返回胜利。
次要特地留神的是,该计划仅限于要更新的表有状态字段,并且刚好要更新状态字段的这种非凡状况,并非所有场景都实用。

7. 加分布式锁

其实后面介绍过的加惟一索引或者加防重表,实质是应用了数据库分布式锁,也属于分布式锁的一种。但因为数据库分布式锁的性能不太好,咱们能够改用:rediszookeeper

鉴于当初很多公司分布式配置核心改用apollonacos,曾经很少用zookeeper了,咱们以redis为例介绍分布式锁。

目前次要有三种形式实现redis的分布式锁:

  1. setNx命令
  2. set命令
  3. Redission框架

每种计划各有利弊,具体实现细节我就不说了,有趣味的敌人能够加我微信找我私聊。

具体流程图如下:

具体步骤:

  1. 用户通过浏览器发动申请,服务端会收集数据,并且生成订单号code作为惟一业务字段。
  2. 应用redis的set命令,将该订单code设置到redis中,同时设置超时工夫。
  3. 判断是否设置胜利,如果设置胜利,阐明是第一次申请,则进行数据操作。
  4. 如果设置失败,阐明是反复申请,则间接返回胜利。
须要特地留神的是:分布式锁肯定要设置一个正当的过期工夫,如果设置过短,无奈无效的避免反复申请。如果设置过长,可能会节约redis的存储空间,须要依据理论业务状况而定。

8. 获取token

除了上述计划之外,还有最初一种应用token的计划。该计划跟之前的所有计划都有点不一样,须要两次申请能力实现一次业务操作。

  1. 第一次申请获取token
  2. 第二次申请带着这个token,实现业务操作。

具体流程图如下:

第一步,先获取token。

第二步,做具体业务操作。

具体步骤:

  1. 用户拜访页面时,浏览器主动发动获取token申请。
  2. 服务端生成token,保留到redis中,而后返回给浏览器。
  3. 用户通过浏览器发动申请时,携带该token。
  4. 在redis中查问该token是否存在,如果不存在,阐明是第一次申请,做则后续的数据操作。
  5. 如果存在,阐明是反复申请,则间接返回胜利。
  6. 在redis中token会在过期工夫之后,被主动删除。

以上计划是针对幂等设计的。

如果是防重设计,流程图要改改:

须要特地留神的是:token必须是全局惟一的。

最初说一句(求关注,别白嫖我)

如果这篇文章对您有所帮忙,或者有所启发的话,帮忙扫描下发二维码关注一下,您的反对是我保持写作最大的能源。

求一键三连:点赞、转发、在看。

关注公众号:【苏三说技术】,在公众号中回复:面试、代码神器、开发手册、工夫治理有超赞的粉丝福利,另外回复:加群,能够跟很多BAT大厂的前辈交换和学习。

 集体公众号

 集体微信