前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >充100,花100,为什么数据库里用户的钱多了100?

充100,花100,为什么数据库里用户的钱多了100?

原创
作者头像
做棵大树
发布2024-06-22 22:43:19
960
发布2024-06-22 22:43:19
举报
文章被收录于专栏:面试必看代码日志

最近赶项目,好久没写文章啦~ 这篇想写好久了,终于写完啦~ 推荐以下俺的公众号,欢迎大家关注呀~

场景是什么样的?

很多情况下都会涉及到一致性的问题,这里我们以常见的支付场景为例。

在常见的支付场景下,我们会有 余额变动 的场景存在。比如说:在购买一个商品的时候,我们要去进行 支付 ;或者在某个APP里看了很多短视频后,我们要对奖励进行提现,这些都涉及到余额的变动。

示例场景
示例场景

如果说对于数据的一致性未能做好保障,那就可能会有 用户充值了100,同时又花掉了100,但是用户的余额还多了100的情况出现。

正常情况下数据会是什么样子?

在实际的场景中,通常我们在执行数据更新前都会有一些业务逻辑。

在实际业务逻辑处理前,我们可能就会从数据库中 读取 对应的数据,然后执行业务逻辑处理,等到处理完成后再将最终结果 更新 回数据库。如下图。

支付流程示例
支付流程示例

假设,用户的余额有 100 元,现在要支付100元,那么我们按照流程,最终写回 0 元是没有问题的。但是这个没有问题的前提是:数据在整个处理逻辑中,未被更改。 也就是只适用于低并发的场景。

会有哪些异常情况?

那什么情况下会有异常呢?数据同时被多个线程操作

无论是高并发又或者说什么分布式,其实都是因为数据被多个线程操作引起了不一致的情况。

我们同样以充值、支付两个场景为例子:

  1. 当两个业务在查询的时候,都从数据库读到了 100 块钱。(因为两个事务可能本身就在两个应用上部署,所以在读的时候互不干扰)
  2. 之后各自基于读取的数据执行不同的业务处理
    1. 充值业务:余额要 + 100,所以最终准备更新为 200
    2. 支付业务:余额要 - 100,所以最终更新为0
  3. 但是因为两个业务下,业务处理逻辑的复杂度不一样/机器性能不一样等,导致耗时不同,最终一个先提交一个后提交。

此时异常出现了!原有金额100元,在充值业务执行慢、提交晚的情况下,数据库余额变成了200。我们丢失了中间支付操作的一次修改,不一致性情况出现了

如果我是顾客我很开心,如果我是员工,我估计就要拎包走人了。😂

有哪些解决方案?

针对上述场景,我们可以采用哪些方式解决呢?考虑现在多是分布式部署,所以肯定优先考虑各个机器都能读取到的共同数据来对数据一致性保证。大树这里列几个可能方案,供大家参考:

分布式锁

通过引入 Redis 或者 zookeeper 这样的支持分布式的中间件来实现分布式锁,在发生可能更改数据的操作时,直接针对记录维度进行上锁,阻塞其他线程进入。

这种悲观锁方案需要引入额外的组件(redis/zk),并且会一定程度降低吞吐量。那有没有轻一点的方式呢?

这时候我们可能就想到CAS的思想了。

CAS 方式乐观锁

对更新字段增加CAS

对于上述充值、支付的场景,我们发现主要关心的其实是 余额 这个核心数据的变更,那我们能不能在更新记录的时候,对余额进行校验呢?

代码语言:sql
复制
update 用户余额信息
set
	余额 = #{更新金额}
WHERE
	用户ID = #{用户ID} AND 余额 = #{期望余额}

其中 AND 余额 = #{期望余额} 就是我们新增的校验逻辑。

此时两个业务场景下一起更新,只有一个会成功,而执行晚的那个因为余额信息发生了改变,则会失败。

诶,此时可能有人想了,我直接更新的时候,进行计算不就好了吗? 就是像下边一样

代码语言:sql
复制
update 用户余额信息
set
	余额 = 余额 - #{本次操作金额}
WHERE
	用户ID = #{用户ID}

看起来是 ok 的,但是这个sql在同一场景同一参数多次执行的情况下(比如接口超时,调用方二次发起请求),会出现不幂等的情况。也就是执行 n 次,余额就会发生 n 次变化,所以 肯定是不OK 的。多充钱会丢饭碗,多扣钱也会丢饭碗。。。😒(关于幂等性咱们可以回头再聊聊)

聊到了 CAS 肯定就会有 ABA 问题的出现。比如说,在上述场景下用户的余额被线程1从 100 变成 200,而后又被线程2变成了 100,此时数据实际发生了改变的,但是在线程3 更新DB的时候,并不能感知到。

此时对于线程3来说,这时候的100其实并不是他读取时的那个100。(有点忒修斯之船的感觉了)

在我们这个场景下可能不会有什么影响,但是在其他的业务场景下,就不一定了。为了解决这个问题,我们可以引入 版本号 来做CAS。

引入版本号做CAS

对更新字段做CAS 不一样的是,我们不关心字段本身,而是关心这个记录的版本。

代码语言:sql
复制
update 用户余额信息
set
	version = version + 1,
	余额 = #{更新金额}
WHERE
	用户ID = #{用户ID} AND version = #{期望version}

在每次更新的时候,我们对版本号进行增加,用于区分数据版本。此时如果有并发操作就会失败,也实现了我们对于数据更新时一致性的保护。

总结

涉及到数据并发修改的场景,要考虑数据的并发一致性。可以根据实际应用场景来选择具体的方案,比如:

  1. 分布式锁
  2. 基于更新字段 或者 version 的乐观锁。需要注意的是 基于更新字段的方式可能存在ABA 问题,需要充分考虑;建议使用 version 方式。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 场景是什么样的?
  • 正常情况下数据会是什么样子?
  • 会有哪些异常情况?
  • 有哪些解决方案?
    • 分布式锁
      • CAS 方式乐观锁
        • 对更新字段增加CAS
        • 引入版本号做CAS
    • 总结
    相关产品与服务
    数据库
    云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档