什么是幂等性

HTTP/1.1中对幂等性的定义是:一次和屡次请求某一个资源对于资源自己应该具备一样的结果(网络超时等问题除外)。也就是说,其任意屡次执行对资源自己所产生的影响均与一次执行的影响相同前端

Methods can also have the property of “idempotence” in that (aside from error or expiration issues) the side-effects of N > 0 identical requests is the same as for a single request.算法

这里须要关注几个重点:数据库

  1. 幂等不只仅只是一次(或屡次)请求对资源没有反作用(好比查询数据库操做,没有增删改,所以没有对数据库有任何影响)。缓存

  2. 幂等还包括第一次请求的时候对资源产生了反作用,可是之后的屡次请求都不会再对资源产生反作用。网络

  3. 幂等关注的是之后的屡次请求是否对资源产生的反作用,而不关注结果。多线程

  4. 网络超时等问题,不是幂等的讨论范围。并发

幂等性是系统服务对外一种承诺(而不是实现),承诺只要调用接口成功,外部屡次调用对系统的影响是一致的。声明为幂等的服务会认为外部调用失败是常态,而且失败以后必然会有重试。异步

什么状况下须要幂等

业务开发中,常常会遇到重复提交的状况,不管是因为网络问题没法收到请求结果而从新发起请求,或是前端的操做抖动而形成重复提交状况。 在交易系统,支付系统这种重复提交形成的问题有尤为明显,好比:分布式

  1. 用户在APP上连续点击了屡次提交订单,后台应该只产生一个订单;ide

  2. 向支付宝发起支付请求,因为网络问题或系统BUG重发,支付宝应该只扣一次钱。 很显然,声明幂等的服务认为,外部调用者会存在屡次调用的状况,为了防止外部屡次调用对系统数据状态的发生屡次改变,将服务设计成幂等。

幂等VS防重

上面例子中小明遇到的问题,只是重复提交的状况,和服务幂等的初衷是不一样的。重复提交是在第一次请求已经成功的状况下,人为的进行屡次操做,致使不知足幂等要求的服务屡次改变状态。而幂等更多使用的状况是第一次请求不知道结果(好比超时)或者失败的异常状况下,发起屡次请求,目的是屡次确认第一次请求成功,却不会因屡次请求而出现屡次的状态变化。

什么状况下须要保证幂等性

以SQL为例,有下面三种场景,只有第三种场景须要开发人员使用其余策略保证幂等性:

  1. SELECT col1 FROM tab1 WHER col2=2,不管执行多少次都不会改变状态,是自然的幂等。

  2. UPDATE tab1 SET col1=1 WHERE col2=2,不管执行成功多少次状态都是一致的,所以也是幂等操做。

  3. UPDATE tab1 SET col1=col1+1 WHERE col2=2,每次执行的结果都会发生变化,这种不是幂等的。

为何要设计幂等性的服务

幂等可使得客户端逻辑处理变得简单,可是却以服务逻辑变得复杂为代价。知足幂等服务的须要在逻辑中至少包含两点:

  1. 首先去查询上一次的执行状态,若是没有则认为是第一次请求

  2. 在服务改变状态的业务逻辑前,保证防重复提交的逻辑

幂等的不足

幂等是为了简化客户端逻辑处理,却增长了服务提供者的逻辑和成本,是否有必要,须要根据具体场景具体分析,所以除了业务上的特殊要求外,尽可能不提供幂等的接口。

  1. 增长了额外控制幂等的业务逻辑,复杂化了业务功能;

  2. 把并行执行的功能改成串行执行,下降了执行效率。

保证幂等策略

幂等须要经过惟一的业务单号来保证。也就是说相同的业务单号,认为是同一笔业务。使用这个惟一的业务单号来确保,后面屡次的相同的业务单号的处理逻辑和执行效果是一致的。 下面以支付为例,在不考虑并发的状况下,实现幂等很简单:①先查询一下订单是否已经支付过,②若是已经支付过,则返回支付成功;若是没有支付,进行支付流程,修改订单状态为‘已支付’。

防重复提交策略

上述的保证幂等方案是分红两步的,第②步依赖第①步的查询结果,没法保证原子性的。在高并发下就会出现下面的状况:第二次请求在第一次请求第②步订单状态尚未修改成‘已支付状态’的状况下到来。既然得出了这个结论,余下的问题也就变得简单:把查询和变动状态操做加锁,将并行操做改成串行操做。

乐观锁

若是只是更新已有的数据,没有必要对业务进行加锁,设计表结构时使用乐观锁,通常经过version来作乐观锁,这样既能保证执行效率,又能保证幂等。例如: UPDATE tab1 SET col1=1,version=version+1 WHERE version=#version# 不过,乐观锁存在失效的状况,就是常说的ABA问题,不过若是version版本一直是自增的就不会出现ABA的状况。(从网上找了一张图片很能说明乐观锁,引用过来,出自Mybatis对乐观锁的支持) 

防重表

使用订单号orderNo作为去重表的惟一索引,每次请求都根据订单号向去重表中插入一条数据。第一次请求查询订单支付状态,固然订单没有支付,进行支付操做,不管成功与否,执行完后更新订单状态为成功或失败,删除去重表中的数据。后续的订单由于表中惟一索引而插入失败,则返回操做失败,直到第一次的请求完成(成功或失败)。能够看出防重表做用是加锁的功能。

分布式锁

这里使用的防重表可使用分布式锁代替,好比Redis。订单发起支付请求,支付系统会去Redis缓存中查询是否存在该订单号的Key,若是不存在,则向Redis增长Key为订单号。查询订单支付已经支付,若是没有则进行支付,支付完成后删除该订单号的Key。经过Redis作到了分布式锁,只有此次订单订单支付请求完成,下次请求才能进来。相比去重表,将放并发作到了缓存中,较为高效。思路相同,同一时间只能完成一次支付请求。 

token令牌

这种方式分红两个阶段:申请token阶段和支付阶段。 第一阶段,在进入到提交订单页面以前,须要订单系统根据用户信息向支付系统发起一次申请token的请求,支付系统将token保存到Redis缓存中,为第二阶段支付使用。 第二阶段,订单系统拿着申请到的token发起支付请求,支付系统会检查Redis中是否存在该token,若是存在,表示第一次发起支付请求,删除缓存中token后开始支付逻辑处理;若是缓存中不存在,表示非法请求。 实际上这里的token是一个信物,支付系统根据token确认,你是你妈的孩子。不足是须要系统间交互两次,流程较上述方法复杂。 

支付缓冲区

把订单的支付请求都快速地接下来,一个快速接单的缓冲管道。后续使用异步任务处理管道中的数据,过滤掉重复的待支付订单。优势是同步转异步,高吞吐。不足是不能及时地返回支付结果,须要后续监听支付结果的异步返回。

CAS(乐观锁)以及ABA问题

独占锁是一种悲观锁,synchronized就是一种独占锁;它假设最坏的状况,而且只有在确保其它线程不会形成干扰的状况下执行,会致使其它全部须要锁的线程挂起直到持有锁的线程释放锁。

所谓乐观锁就是每次不加锁,假设没有冲突而去完成某项操做;若是发生冲突了那就去重试,直到成功为止。

CAS(Compare And Swap)是一种有名的无锁算法。CAS算法是乐观锁的一种实现。CAS有3个操做数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改成B并返回true,不然返回false。

注:synchronized和ReentrantLock都是悲观锁。

注:何时使用悲观锁效率更高、什么使用使用乐观锁效率更高,要根据实际状况来判断选择。

什么是CAS机制
CAS是英文单词Compare And Swap的缩写,翻译过来就是比较并替换。

CAS机制当中使用了3个基本操做数:内存地址V,旧的预期值A,要修改的新值B。

更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改成B。

CAS是英文单词Compare And Swap的缩写,翻译过来就是比较并替换。

CAS机制当中使用了3个基本操做数:内存地址V,旧的预期值A,要修改的新值B。

更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改成B。

这样说或许有些抽象,咱们来看一个例子:

1.在内存地址V当中,存储着值为10的变量。

 

2.此时线程1想要把变量的值增长1。对线程1来讲,旧的预期值A=10,要修改的新值B=11。

 

3.在线程1要提交更新以前,另外一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。

 

4.线程1开始提交更新,首先进行A和地址V的实际值比较(Compare),发现A不等于V的实际值,提交失败。

 

5.线程1从新获取内存地址V的当前值,并从新计算想要修改的新值。此时对线程1来讲,A=11,B=12。这个从新尝试的过程被称为自旋。

 

6.这一次比较幸运,没有其余线程改变地址V的值。线程1进行Compare,发现A和地址V的实际值是相等的。

 

7.线程1进行SWAP,把地址V的值替换为B,也就是12。

 

从思想上来讲,Synchronized属于悲观锁,悲观地认为程序中的并发状况严重,因此严防死守。CAS属于乐观锁,乐观地认为程序中的并发状况不那么严重,因此让线程不断去尝试更新。

CAS的优缺点:
乐观锁避免了悲观锁独占对象的现象,同时也提升了并发性能,乐观锁是对悲观锁的改进,虽然它也有缺点,但它确实已经成为提升并发性能的主要手段,并且jdk中的并发包也大量使用基于CAS的乐观锁。但它也有缺点,以下:

1.CPU可能开销较大
在并发量比较高的状况下,若是许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。

2.不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子性操做,而不能保证整个代码块的原子性。好比须要保证3个变量共同进行原子性的更新,就不得不使用悲观锁了。

3.ABA问题。
CAS的核心思想是经过比对内存值与预期值是否同样而判断内存值是否被改过,但这个判断逻辑不严谨,假如内存值原来是A,后来被一条线程改成B,最后又被改为了A,则CAS认为此内存值并无发生改变,但其实是有被其余线程改过的,这种状况对依赖过程值的情景的运算结果影响很大。解决的思路是引入版本号,每次变量更新都把版本号加一。

ABA问题:
线程1准备用CAS修改变量值A,在此以前,其它线程将变量的值由A替换为B,又由B替换为A,而后线程1执行CAS时发现变量的值仍然为A,因此CAS成功。但实际上这时的现场已经和最初不一样了。

 

 

ABA问题处理:
思路:解决ABA最简单的方案就是给值加一个修改版本号,每次值变化,都会修改它版本号,CAS操做时都对比此版本号。

 

JAVA中ABA中解决方案(AtomicStampedReference/AtomicMarkableReference)

AtomicStampedReference 本质是有一个int 值做为版本号,每次更改前先取到这个int值的版本号,等到修改的时候,比较当前版本号与当前线程持有的版本号是否一致,若是一致,则进行修改,并将版本号+1(固然加多少或减多少都是能够本身定义的),在zookeeper中保持数据的一致性也是用的这种方式;

AtomicMarkableReference则是将一个boolean值做是否有更改的标记,本质就是它的版本号只有两个,true和false,修改的时候在这两个版本号之间来回切换,这样作并不能解决ABA的问题,只是会下降ABA问题发生的概率而已;

相关文章
相关标签/搜索