欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

深入理解幂等性和乐观锁

最编程 2024-05-05 12:38:01
...

什么是幂等性?

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

为了便于理解我们先说场景:(什么情况下需要幂等)

业务开发中,经常会遇到重复提交的情况,无论是由于网络问题无法收到请求结果而重新发起请求,或是前端的操作抖动而造成重复提交情况。 在交易系统,支付系统这种重复(也许是高并发造成)提交造成的问题有尤其明显,比如:

1、用户在APP上连续点击了多次提交订单,后台应该只产生一个订单;(假设第一次点击还没返回结果,但是又进行了第二次点击调用)这里就两次同时调用的意思

2、向支付宝发起支付请求,由于网络问题或系统BUG重发,支付宝应该只扣一次钱。

3、扣减商品库存时,上一次请求库存减1的请求还没返回,第二次库存减2的请求几乎同时又来了;

很显然,声明幂等的服务认为,外部调用者会存在多次调用的情况,为了防止外部多次调用对系统数据状态的发生多次改变,让程序返回结果可控,将服务设计成幂等。

结合我们实际开发过程中再谈谈什么情况下需要保证幂等:

以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、把并行执行的功能改为串行执行,降低了执行效率。

保证幂等策略

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

防重复提交策略

上述的保证幂等方案是分成两步的,第②步依赖第①步的查询结果,无法保证原子性的。在高并发下就会出现一个情况:

第二次请求在第一次请求第②步订单状态还没有修改为‘已支付状态’的情况下到来。这个时候第二个请求中的逻辑也会得到执行,结果将会变得不可控。(这里的执行不单单只是将状态改为已支付;也许会执行二次扣款,结果可能会变成扣两次款)

看完上述的情况,大家应该对幂等性有了一定的理解,再次梳理一下;

在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。有些天然幂等的函数,例如,"select"及"setTrue()"函数就是天然幂等函数,无论多次执行,其结果都是一样的,更复杂的操作幂等保证是利用唯一交易号(流水号)实现.

ok,大概总结完幂等性的理解之后,我们来讨论如何保证幂等;(就是我们上面说的利用唯一流水号)

这里就引出了另外一个东西叫:锁--->又分为“乐观锁”与“悲观锁”;

我在网上找了个场景图,便于理解;


高并发场景,两个客户端同时调用报名接口


我们要避免上图的情况,就可以用乐观锁或者悲观锁,进行并发控制。这里要说明的是,无论乐观锁还是悲观锁它都只是一种实现方式,一种思想,一种手段,与数据库的事务锁不是一个概念,但是实现思路是相通的;

悲观锁(Pessimistic Lock)

当我们要对一个数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。这种借助数据库锁机制,在修改数据之前先锁定,再修改的方式被称之为悲观并发控制(又名“悲观锁”,Pessimistic Concurrency Control,缩写“PCC”)。

上面就是悲观锁的概念,其实很多程序员经常用,我也经常用,我把它理解为并行变成串行,就是没有了并发;坏处就是一个字:慢,有可能死锁。好处就是安全。

好了,悲观锁不是今天的主题;主要想记录乐观锁,这个东西很有意思:

乐观锁( Optimistic Locking )

乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回给用户错误的信息,让用户决定如何去做。

乐观并发控制相信事务之间的数据竞争(data race)的概率是比较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁。

乐观锁实现方式

使用乐观锁就不需要借助数据库的锁机制了。

乐观锁的概念中其实已经阐述了它的具体实现细节。主要就是两个步骤:冲突检测和数据更新。其实现方式有一种比较典型的就是CAS(Compare and Swap)。

CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

比如前面的扣减库存问题,通过乐观锁可以实现如下:

记住乐观锁是一种人为的实现方式,不需要借助数据库的事务锁

以上,我们在更新之前,先查询一下库存表中当前库存数(quantity),然后在做update的时候,以库存数作为一个修改条件。当我们提交更新的时候,判断数据库表对应记录的当前库存数与第一次取出来的库存数进行比对,如果数据库表当前库存数与第一次取出来的库存数相等,则予以更新,否则认为是过期数据。

重点来了:上面操作存在一个比较重要的问题,即传说中的ABA问题:(就是2个请求同时进来的高并发问题)

比如说一个线程one从数据库中取出库存数3,这时候另一个线程two也从数据库中取出库存数3,并且two进行了一些操作变成了2,然后two又将库存数变成3,这时候线程one进行CAS操作发现数据库中仍然是3,然后one操作成功。尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。

有一个比较好的办法可以解决ABA问题,那就是通过一个单独的可以顺序递增的version字段。改为以下方式即可:

就是第二个线程执行应该是失败的,人为可控,失败就走失败的逻辑

乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行+1操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现ABA问题,因为版本号只会增加不会减少。

除了version以外,还可以使用时间戳,因为时间戳天然具有顺序递增性。

以上SQL其实还是有一定的问题的,就是一旦遇上高并发的时候,就只有一个线程可以修改成功,那么就会存在大量的失败。

对于像淘宝这样的电商网站,高并发是常有的事,总让用户感知到失败显然是不合理的。所以,还是要想办法减少乐观锁的粒度的。

有一条比较好的建议,可以减小乐观锁力度,最大程度的提升吞吐率,提高并发能力!如下:


优化

以上SQL语句中,如果用户下单数为1,则通过quantity - 1 > 0的方式进行乐观锁控制。

以上update语句,在执行过程中,会在一次原子操作中自己查询一遍quantity的值,并将其扣减掉1。

高并发环境下锁粒度把控是一门重要的学问,选择一个好的锁,在保证数据安全的情况下,可以大大提升吞吐率,进而提升性能。

如何选择

在乐观锁与悲观锁的选择上面,主要看下两者的区别以及适用场景就可以了。

乐观锁并未真正加锁,效率高。一旦锁的粒度掌握不好,更新失败的概率就会比较高,容易发生业务失败。

悲观锁依赖数据库锁,效率低。更新失败的概率比较低。

随着互联网三高架构(高并发、高性能、高可用)的提出,悲观锁已经越来越少的被使用到生产环境中了,尤其是并发量比较大的业务场景。


以上内容摘抄至两篇博客,只为自己记录理解及分享;如有侵权,请联系删除!

注明出处:感谢作者

作者:MChopin      链接:https://www.jianshu.com/p/d2ac26ca6525

作者: java懒洋洋  链接:  https://www.cnblogs.com/javalyy/p/8882144.html