钱包系统cash-in/cash-out并发更新余额异常的最优解决方案咨询
问题根因
这是典型的并发丢失更新问题:多个请求同时读取到相同的余额值,各自在应用层计算后写入数据库,后提交的更新会覆盖前一个请求的修改,最终导致余额计算结果不符合预期。
最优推荐方案
优先选择数据库层面的原子更新方案,实现成本最低、性能最高,完全可以解决90%以上的钱包并发场景:
原子更新(首选)
不要在应用层做余额计算,直接把运算逻辑放到SQL中执行,单条UPDATE语句本身具备原子性,数据库会自动对操作行加排他锁,不会出现并发覆盖问题:
-- 充值操作 UPDATE Wallet SET balance = balance + ? WHERE idWallet = ?; -- 提现操作,自带余额非负校验 UPDATE Wallet SET balance = balance - ? WHERE idWallet = ? AND balance >= ?;
执行后判断返回的影响行数:如果为0说明余额不足或者钱包不存在,直接返回对应错误即可,完全不需要先查询余额再在应用层计算。
如果业务逻辑必须先读取余额做复杂校验(比如叠加优惠券、风控规则判断),再执行更新,可选择以下两种方案:
方案1:悲观锁(适用并发较高的金融场景)
查询余额的时候加排他锁,确保当前事务持有锁期间,其他事务无法读取或修改该行数据:
-- 注意必须在事务中执行,否则查询结束后锁会自动释放 BEGIN; SELECT * FROM Wallet WHERE idUser = x FOR UPDATE; -- 这里执行你的业务校验逻辑,计算最终要更新的余额 UPDATE Wallet SET balance = ? WHERE idWallet = x; COMMIT;
方案2:乐观锁(适用并发较低的场景)
给Wallet表新增一个version整型字段,默认值为0,每次更新时版本号自增:
-- 第一步:查询余额和版本号 SELECT balance, version FROM Wallet WHERE idUser = x; -- 第二步:应用层做业务校验,计算新的余额 -- 第三步:执行更新,带版本号条件 UPDATE Wallet SET balance = ?, version = version + 1 WHERE idWallet = x AND version = ?;
如果UPDATE返回的影响行数为0,说明期间有其他请求已经修改了该用户的钱包数据,可选择重试操作或者直接返回操作失败提示用户重试。
你提到的两个方案评估
- 队列串行处理:仅适合并发量极高、单数据库行锁已经成为瓶颈,或者钱包操作附带非常多复杂业务逻辑的场景。注意不要把所有用户的请求放到同一个队列,必须按用户ID做分片,同一个用户的所有钱包请求进入同一个子队列串行执行,避免整体性能过低。该方案实现复杂度较高,需要额外处理队列可靠性、消息重试、幂等性问题,中小流量场景没必要用。
- 锁表:完全不推荐。锁表会导致所有用户的钱包操作全部阻塞,只要有一个用户的操作执行慢,整个系统的钱包功能都会瘫痪,性能极差,不适合任何生产环境使用。
选型建议
普通业务场景优先选择原子UPDATE+可选悲观锁的方案,实现成本最低,性能足够支撑绝大多数场景,稳定性也有数据库层面的保障。只有当单库性能确实遇到瓶颈时,再考虑队列等分布式方案。
内容的提问来源于stack exchange,提问作者Thiago Gabriel Braz
相关产品推荐
相关产品推荐

