最近赶项目,好久没写文章啦~ 这篇想写好久了,终于写完啦~ 推荐以下俺的公众号,欢迎大家关注呀~
很多情况下都会涉及到一致性的问题,这里我们以常见的支付场景为例。
在常见的支付场景下,我们会有 余额变动 的场景存在。比如说:在购买一个商品的时候,我们要去进行 支付 ;或者在某个APP里看了很多短视频后,我们要对奖励进行提现,这些都涉及到余额的变动。
如果说对于数据的一致性未能做好保障,那就可能会有 用户充值了100,同时又花掉了100,但是用户的余额还多了100的情况出现。
在实际的场景中,通常我们在执行数据更新前都会有一些业务逻辑。
在实际业务逻辑处理前,我们可能就会从数据库中 读取 对应的数据,然后执行业务逻辑处理,等到处理完成后再将最终结果 更新 回数据库。如下图。
假设,用户的余额有 100 元,现在要支付100元,那么我们按照流程,最终写回 0 元是没有问题的。但是这个没有问题的前提是:数据在整个处理逻辑中,未被更改。 也就是只适用于低并发的场景。
那什么情况下会有异常呢?数据同时被多个线程操作
无论是高并发又或者说什么分布式,其实都是因为数据被多个线程操作引起了不一致的情况。
我们同样以充值、支付两个场景为例子:
此时异常出现了!原有金额100元,在充值业务执行慢、提交晚的情况下,数据库余额变成了200。我们丢失了中间支付操作的一次修改,不一致性情况出现了。
如果我是顾客我很开心,如果我是员工,我估计就要拎包走人了。😂
针对上述场景,我们可以采用哪些方式解决呢?考虑现在多是分布式部署,所以肯定优先考虑各个机器都能读取到的共同数据来对数据一致性保证。大树这里列几个可能方案,供大家参考:
通过引入 Redis 或者 zookeeper 这样的支持分布式的中间件来实现分布式锁,在发生可能更改数据的操作时,直接针对记录维度进行上锁,阻塞其他线程进入。
这种悲观锁方案需要引入额外的组件(redis/zk),并且会一定程度降低吞吐量。那有没有轻一点的方式呢?
这时候我们可能就想到CAS的思想了。
对于上述充值、支付的场景,我们发现主要关心的其实是 余额 这个核心数据的变更,那我们能不能在更新记录的时候,对余额进行校验呢?
update 用户余额信息
set
余额 = #{更新金额}
WHERE
用户ID = #{用户ID} AND 余额 = #{期望余额}
其中 AND 余额 = #{期望余额}
就是我们新增的校验逻辑。
此时两个业务场景下一起更新,只有一个会成功,而执行晚的那个因为余额信息发生了改变,则会失败。
诶,此时可能有人想了,我直接更新的时候,进行计算不就好了吗? 就是像下边一样
update 用户余额信息
set
余额 = 余额 - #{本次操作金额}
WHERE
用户ID = #{用户ID}
看起来是 ok 的,但是这个sql在同一场景同一参数多次执行的情况下(比如接口超时,调用方二次发起请求),会出现不幂等的情况。也就是执行 n 次,余额就会发生 n 次变化,所以 肯定是不OK 的。多充钱会丢饭碗,多扣钱也会丢饭碗。。。😒(关于幂等性咱们可以回头再聊聊)
聊到了 CAS 肯定就会有 ABA 问题的出现。比如说,在上述场景下用户的余额被线程1从 100 变成 200,而后又被线程2变成了 100,此时数据实际发生了改变的,但是在线程3 更新DB的时候,并不能感知到。
此时对于线程3来说,这时候的100其实并不是他读取时的那个100。(有点忒修斯之船的感觉了)
在我们这个场景下可能不会有什么影响,但是在其他的业务场景下,就不一定了。为了解决这个问题,我们可以引入 版本号 来做CAS。
和 对更新字段做CAS 不一样的是,我们不关心字段本身,而是关心这个记录的版本。
update 用户余额信息
set
version = version + 1,
余额 = #{更新金额}
WHERE
用户ID = #{用户ID} AND version = #{期望version}
在每次更新的时候,我们对版本号进行增加,用于区分数据版本。此时如果有并发操作就会失败,也实现了我们对于数据更新时一致性的保护。
涉及到数据并发修改的场景,要考虑数据的并发一致性。可以根据实际应用场景来选择具体的方案,比如:
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。