巧用CAS解决数据一致性问题

原文地址:http://mp.weixin.qq.com/s/_XlzbmBSj_i-S2PkE5tI_w

 

缘起:在高并发的分布式环境下,对于数据的查询与修改容易引发一致性问题,本文将分享一种非常简单但有效的优化方法。

 

一、业务场景

业务场景为,购买商品的过程要对余额进行查询与修改,大致的业务流程如下:

(1)从数据库查询用户现有余额 SELECT money FROM t_yue WHERE uid=$uid,不妨设查询出来的$old_money=100元

 

(2)业务层实施业务逻辑,比如购买一个80元的商品,并且打九折

if($old_money> 80*0.9) $new_money=$old_money-80*0.9=28

 

(3)将数据库中的余额进行修改 UPDAtE t_yue SET money=$new_money WHERE uid=$uid

 

在并发量低的情况下,这个流程没有任何问题,原有金额100元,购买了80元的九折商品(72元),剩余28元。

 

二、潜在的问题

在分布式环境中,如果并发量很大,这种“查询+修改”的业务很容易出现数据不一致。极限情况下,可能出现这样的异常流程:

(1)业务1和业务2同时查询余额,是100元

 

(2)业务1和业务2进行逻辑计算,算出各自业务的余额,假设业务1算出的余额是28元,业务2算出的余额是38元

 

 

(3)业务1对数据库中的余额先进行修改,设置成28元。

业务2对数据库中的余额后进行修改,设置成38元。

此时异常出现了,原有金额100元,业务1扣除了72元,业务2扣除了62元,最后剩余38元。

 

三、问题原因

高并发环境下,对同一个数据的并发读(两边都读出余额是100)与并发写(一个写回28,一个写回38)导致的数据一致性问题。

 

四、原因分析

业务1的写回:原有金额100,这是一个初始状态,写回金额28,理论上只有在原有金额为100的时候才允许写回成功,这一步没问题。

业务2的写回:的原有金额100,这是一个初始状态,写回金额38,理论上只有在原有金额为100的时候才允许写回成功,可实际上,这个时候数据库中的金额已经变为28了,这一步的写操作不应该成功。

 

五、简易解决方案

在set写回的时候,加上初始状态的条件compare,只有初始状态不变时,才允许set写回成功,这正是大家常说的“Compare And Swap”(CAS,比较并替换),也可以叫做“Compare And Set”,是一种常见的降低读写锁冲突,保证数据一致性的方法,类似于乐观锁。

 

六、业务的升级

业务线使用CAS解决高并发时数据一致性问题,只需要在进行set操作时,compare一下初始值,如果初始值变换,不允许set成功。

对于上文中的业务场景,只需要将“UPDAtEt_yue SET money=$new_money WHERE uid=$uid”升级为

“UPDAtE t_yue SETmoney=$new_money WHERE uid=$uid AND money=$old_money”即可。

并发操作发生时:

业务1执行 => UPDAtE t_yue SET money=28 WHERE uid=$uid AND money=100

业务2执行 => UPDAtE t_yue SET money=38 WHERE uid=$uid AND money=100

【这两个操作同时进行时,只能有一个执行成功】。

 

七、怎么判断哪个执行成功,哪个执行失败

set操作,其实无所谓成功或者失败,业务能通过affect rows得知哪个修改没有成功:

执行成功的业务,affect rows为1

执行失败的业务,affect rows为0

 

八、总结

高并发“查询并修改”的场景,可以用CAS(Compare and Swap)的方式解决数据一致性问题。对应到业务,即在set的时候,加上初始条件的比对

CAS对比Synchronized的缺点:

1、CAS对cpu开销比较大,并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却一直更新不成功,循环往复,会给cpu带来很大压力

2、不能保证代码块的原子性,CAS机制所保证的只是一个变量的原子性操作

3、ABA问题,CAS容易出现ABA问题,有三个线程使用CAS更新某一变量:

(1)线程1:获取当前值A,期望更新为B

(2)线程2:获取当前值A,期望更新为B

(3)线程3:期望更新为A

接下来,线程1执行成功,把当前值更新为B,线程2由于某种原因被阻塞,没有做更新操作,线程3在线程1更新之后,获取当前值B,线程3成功把当前值B更新

为A,此时线程2恢复运行状态,由于阻塞之前获取“当前值”为A,并且经过compare检测,内存地址中的实际值也是A,所以检测通过,成功将变量A更新为B。

这个过程中,线程2获取的变量值A是一个旧值,尽管和当前的实际值相同,当时内存地址中的变量已经经历了A->B->A的改变。

实际例子:

假设有一个遵循CAS原理的提款机,小灰有100元存款,需要提款50元;由于提款机硬件出现问题,小灰的提款操作被同时提交了两次,开启了两个线程去处理,

两个线程都是获取当前值100元,要更新为50元。理想情况下,应该一个线程更新成功,另一个线程更新失败,小灰的存款只被扣取一

次。线程1首先执行成功,把余额从100改成50,线程2由于某种原因阻塞了,这时候小灰的妈妈刚好给小灰汇款50元,使用线程3进行处理。

(1)线程1(提款机):获取当前值100,成功更新为50

(2)线程2(提款机):获取当前值100,更新为50(BLOCK阻塞

(3)线程3(小灰妈):获取当前值50,期望更新为100

线程2仍然是阻塞状态,线程3执行成功,把余额从50改成100,此时线程2恢复运行,通过CAS检测阻塞之前获取的“当前值”100和此时实际存款100,检测通过,

成功更新变量值100为50。小灰的余额被扣取了两次。

从思想上来说,CAS属于乐观锁,Synchronized属于悲观锁,CAS代码性能比Synchronized高,CAS的ABA问题可以使用版本号进行解决,java的AtomicInteger、AtomicLong、AtomicBoolean等原子类操作都使用了CAS和版本号。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值