SQL并发执行时交易表余额计算是否会读取相同旧余额值
结论
一定会出现你描述的余额计算错误,这是数据库并发操作场景下非常典型的竞态条件问题,只要没有做额外的并发控制,并发请求量上来后几乎必然出现余额对不上的情况。
错误触发逻辑
我们可以用一个非常简单的时序例子还原问题,假设当前账户最后一条交易记录的balance值为100元,同时发起两笔交易:一笔转入30元、一笔转入20元:
- 第一步:转入30元的请求A先执行查询最新余额的逻辑,读到当前最新余额是100
- 第二步:转入20元的请求B在请求A还没完成新记录写入的时候,也执行了查询最新余额的逻辑,同样读到值100
- 第三步:请求A计算新余额为100+30=130,写入新的交易记录
- 第四步:请求B基于之前读到的100计算新余额为100+20=120,写入新的交易记录
最终数据库里最后一条交易的余额是120元,但两笔交易都完成后的正确余额应该是100+30+20=150元,凭空少了30元,就是你担心的错误结果。
原有更新逻辑示意图如下:

适合初学者的常见修复方案
你可以根据自己的业务场景选其中一种实现,都能彻底规避这个问题:
- 悲观锁方案
用数据库事务加行锁,保证同一时间只有一个请求能走完「查最新余额-计算新余额-写入新记录」的全流程,其他请求必须等前一个请求写完才能读取最新余额,不会出现读旧值的情况,核心逻辑参考:-- 开启事务 BEGIN; -- 加锁查询最新余额,此时其他并发请求会在这里阻塞等待,直到当前事务提交 SELECT balance FROM 交易表 ORDER BY id DESC LIMIT 1 FOR UPDATE; -- 完成新余额计算后,插入新的交易记录 INSERT INTO 交易表(description, amount, balance, datetime) VALUES (对应交易描述, 本次交易金额, 计算出的新余额, 交易时间); -- 提交事务释放锁,其他等待的请求此时可以读到最新插入的余额 COMMIT; - 原子更新方案(最推荐)
不要在交易流水表中存balance字段,单独建一张账户表存每个账户的总余额,更新余额时直接用数据库原生的原子更新语句,不需要先查询再计算,从根源上避免读旧值的问题:
交易流水表只需要存-- 直接在原有余额基础上累加本次交易金额,这条语句的执行是数据库原子保障的,不会出现并发错误 UPDATE 账户表 SET balance = balance + 本次交易金额 WHERE 账户id = 对应交易的账户id;id、description、amount、datetime、关联账户id即可,需要查询某笔交易发生后的余额时,可以通过流水临时统计,也可以在查询时关联账户表获取。 - 乐观锁方案
给表增加一个version版本号字段,每次查询余额时同时查出当前版本号,写入新记录时校验版本号是否和自己之前查到的一致,如果不一致说明期间有其他请求写入了新数据,当前请求回滚重试即可,适合并发量不高的场景。
内容的提问来源于stack exchange,提问作者Victoria Molleja
相关产品推荐
相关产品推荐

