当前位置: 首页 > 科技观察

如何保证并发推导的数据一致性?

时间:2023-03-16 02:16:09 科技观察

继续回答星球水友的问题。沉先生,我们有生意。在同一用户并发“查询、逻辑计算、扣费”的情况下,可能会出现余额不一致的情况。有什么优化方法吗?扣费的业务场景是什么?用户在购买商品的过程中,需要查询和修改余额。一般的业务流程如下:第一步从数据库中查询用户现有余额:SELECTmoneyFROMt_yueWHEREuid=$uid;最好设置查询$old_money=100元。第二步,业务层实现业务逻辑计算,例如:先查询购买商品的价格,比如80元;然后查看商品是否有活动,活动折扣,比如10%off;比较余额是否足够,只有足够了才往下走;if($old_money>80*0.9){$new_money=$old_money-80*0.9=28}else{return"Notenoughminerals";}第三步是修改数据库中的余额。UPDATEt_yueSETmoney=$new_moneyWHEREuid=$uid;在低并发的情况下,这个过程是没有问题的。原金额100元,购买满80元的10折产品(72元),还剩28元。同一用户并发扣费可能会出现什么问题?在分布式环境下,如果并发量很大,这种“查询+修改”的业务有一定概率出现数据不一致。极端情况下,可能会出现这样的异常流程:第1步,业务1和业务2同时查询余额,余额为100元。画外音:这些并发查询是在不同的站点实例/服务实例上做的,进程内互斥肯定解决不了。第二步:业务1和业务2同时进行逻辑计算,计算出各自业务的余额。假设商家1计算的余额为28元,商家2计算的余额为38元。第三步,业务1先修改数据库中的余额,设置为28元。业务2修改数据库中的余额,设置为38元。这时,异常出现了。原金额100元,业务1扣72元,业务2扣62元,最后还剩38元。画外音:假设业务1先回写余额,业务2再回写余额。常见的解决方案?对于这种情况,当同一个用户并发扣款时,有小概率会出现异常。可以对每个用户进行分布式锁互斥,例如:在redis/zk中抢一个key继续操作,否则禁止操作。这种悲观锁方案确实可行,但是会引入额外的组件(redis/zk),会降低吞吐量。对于小概率的不一致,有乐观锁的解决方案吗?进一步分析并发扣款发现:(1)业务1回写时,旧余额为100,为初始状态;新余额为28,这是一个初始状态结束状态。理论上只有当旧余额为100时,新余额才能成功回写。业务1并发回写时,旧余额确实是100,应该回写成功。(2)业务2回写时,旧余额为100,为初始状态;新余额为28,这是一个结束状态。理论上只有当旧余额为100时,新余额才能成功回写。但实际上此时数据库中的amount已经变成了28,所以业务2的并发回写应该不会成功。如何低成本实现乐观锁?回写集合时,添加初始状态条件比较。只有保持初始状态不变,才允许成功回写集合。CompareAndSet(CAS)是一种常见的减少读写锁冲突的方法,一种保证数据一致性的方法。这个时候怎么改业务?使用CAS解决高并发下的数据一致性问题。执行设置操作时只需要比较初始值即可。如果初始值发生变化,则不允许设置成功。具体针对这种情况,只需要升级:UPDATEt_yueSETmoney=$new_moneyWHEREuid=$uid;至:UPDATEt_yueSETmoney=$new_moneyWHEREuid=$uidANDmoney=$old_money;。并发时:业务1执行:UPDATEt_yueSETmoney=28WHEREuid=$uidANDmoney=100;业务2执行:UPDATEt_yueSETmoney=38WHEREuid=$uidANDmoney=100;当这两个操作同时执行时,只有其中一个可以执行成功。如何判断哪个并发执行成功,哪个并发执行失败?set操作,其实成功与失败无关紧要。通过affectrows判断业务:如果回写成功,则affectrows为1;如果回写失败,则影响行数为0。总结high在并发“查询和修改”场景下,可以使用CAS(CompareandSet)方式解决数据一致性问题。对应业务,就是在设置的时候,加上初始条件的比较。优化不难,只改了半行SQL,但确实能解决问题。但是希望大家有所收获,想法比结论更重要。【本文为专栏作者《58神剑》原创稿件,转载请联系原作者】点此阅读更多该作者好文