上一篇回答星球水友问题的文章《并发扣款,如何保证数据的一致性?》提到:使用CAS乐观锁可以在不影响吞吐量的情况下尽可能保证数据的一致性。大家的评论很多,大概分为以下几类:是不是ABA出了问题?为什么不能用:UPDATEt_yueSETmoneymoney=money-$diffANDmoney>=$diff;能不能用redis事务来扣余额;画外音:请务必阅读序言文章:《并发扣款,如何保证数据的一致性?》。问题很多,今天先说第一个问题,ABA。ABA问题是什么?CAS乐观锁机制确实可以提高吞吐量和保证一致性,但极端情况下可能会出现ABA问题。考虑以下操作:并发1(上图):获取数据初始值为A,后续计划实现CAS乐观锁。当预期的数据还是A时,就可以修改成功了。并发2:修改数据为B并发3:将数据修改回A并发1(下):CAS乐观锁,检测发现初始值还是A,修改数据。在上面的并发环境中,并发1在修改数据时,虽然还是A,但是已经不是初始条件的A了。如果把A改成B,再把B改成A,这个A就不再是A了,但是数据修改成功了,可能会出错。这就是所谓的CAS引起的ABA问题。在balance操作中,ABA问题不会影响业务,因为对于“balance”属性来说,前面的A是100的余额,后面的A是100的余额,本质上是一样的。但其他场景可能不是这样。举个栈操作的例子:并发1(上):读取栈顶元素为“A1”1(下):实现CAS乐观锁,发现栈顶还是“A1”,所以改为A2,此时会出现系统错误,因为这个“A1”不是另一个“A1”,ABA问题怎么优化呢?之所以会出现ABA问题,是因为在CAS过程中对“值”进行了简单的校验,在某些情况下,相同的“值”不会引入错误的业务逻辑(如余额),而在某些情况下,虽然“价值”是一样的,它不再是原始数据了(例如堆栈)。因此,CAS不能只比对“值”,还必须保证是原始数据,才能成功修改。一种常见的做法是将“值”比较升级为“版本号”比较,一个版本的数据,版本发生变化,即使值相同,修改也不应该成功。以余额并发读写为例,引入版本号的具体做法如下:(1)余额表需要升级。t_yue(uid,money)升级为:t_yue(uid,money,version)(2)查询余额时,同时查询版本号。SELECTmoneyFROMt_yueWHEREsid=$sid升级为:SELECTmoney,versionFROMt_yueWHEREsid=$sid假设有并发操作,会查询版本号。(3)设置余额时,版本号必须相同,版本号必须修改。老版本“值”比较:UPDATEt_yueSETmoney=38WHEREuid=$uidANDmoney=100升级到“版本号”比较:UPDATEt_yueSETmoney=38,version=$version_newWHEREuid=$uidANDversion=$version_old此时,假设有并发操作,第一个operationrequest会修改版本号,并发操作失败。画外音:版本是通用的,这个例子只是以版本为例,其实这个例子可以比作余额的“值”。总结一下select&set的业务场景,并发时会出现一致性问题。基于“值”的CAS乐观锁可能会导致ABA问题。CAS乐观锁在修改的时候一定要保证“这个数据”是“那个数据”,而且要由“值”来决定。对比,优化到“版本号”对比思路比结论更重要。【本文为专栏作者《58神剑》原创稿件,转载请联系原作者】点此阅读更多该作者好文
