You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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元,就是你担心的错误结果。
原有更新逻辑示意图如下:
交易逻辑示意图1
交易逻辑示意图2

适合初学者的常见修复方案

你可以根据自己的业务场景选其中一种实现,都能彻底规避这个问题:

  • 悲观锁方案
    用数据库事务加行锁,保证同一时间只有一个请求能走完「查最新余额-计算新余额-写入新记录」的全流程,其他请求必须等前一个请求写完才能读取最新余额,不会出现读旧值的情况,核心逻辑参考:
    -- 开启事务
    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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 14:24:20