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

如何安全向数据库新增资金操作,避免高并发下出现负余额问题?

并发扣款导致余额为负的解决方案

你遇到的是典型的并发读写场景下的数据一致性问题,以下是3种可落地的解决方案,按实现复杂度、可靠性、适用场景分类:

方案1:数据库排他锁(悲观锁,实现简单可靠性高)

  • 适用场景:并发冲突概率高,对资金一致性要求极高的场景
  • 实现步骤:
  1. 开启数据库事务
  2. 执行余额查询SQL并加排他锁,语句如下:
SELECT SUM(amount) FROM fund_operations WHERE user_id = #{用户ID} FOR UPDATE;
  1. 校验查询得到的余额是否大于等于购买金额,不满足则直接回滚事务,返回余额不足
  2. 校验通过则插入扣款交易记录(扣款金额统一存为负值)
  3. 提交事务,自动释放排他锁

原理:排他锁会阻塞其他事务对当前用户交易记录的查询请求,直到当前事务完成,保证同一时间只有一个请求能操作该用户的资金,不会出现并发校验都通过的问题。

方案2:原子插入校验(无锁实现,性能更高)

  • 适用场景:并发冲突概率中等,想要更高处理性能的场景
  • 直接执行单条原子INSERT语句,将余额校验逻辑嵌入SQL中,利用数据库的原子执行特性避免并发问题:
INSERT INTO fund_operations (user_id, amount, op_type, create_time)
SELECT #{用户ID}, -#{购买金额}, 'purchase', NOW()
FROM DUAL
WHERE (SELECT SUM(amount) FROM fund_operations WHERE user_id = #{用户ID}) >= #{购买金额};
  • 执行后判断SQL的影响行数:返回1代表扣款成功,返回0代表余额不足扣款失败

原理:单条DML语句的执行是原子性的,数据库会保证整个校验+插入的过程不会被其他请求打断,不需要额外加锁也不会出现数据不一致。

方案3:冗余余额表+乐观锁(适合大业务量场景)

  • 适用场景:用户交易记录量级极大,每次SUM计算余额性能低下的场景
  • 实现步骤:
  1. 新增user_balance冗余表存储用户当前余额,字段包括user_id(用户ID)、balance(当前余额)、version(版本号)
  2. 用户发起购买请求时,先查询该用户的余额和版本号:
SELECT balance, version FROM user_balance WHERE user_id = #{用户ID};
  1. 校验余额大于等于购买金额后,执行带版本号校验的更新语句:
UPDATE user_balance 
SET balance = balance - #{购买金额}, version = version + 1 
WHERE user_id = #{用户ID} AND version = #{之前查询到的版本号};
  1. 若更新影响行数为1,说明更新成功,再插入对应的扣款交易记录;若影响行数为0,说明期间有其他请求修改了用户余额,可选择重试或者直接返回操作失败

注意:余额表的更新和交易记录的插入需要放在同一个本地事务中执行,保证两者数据一致。

通用注意事项

  • 所有金额相关字段必须使用定点数类型(如MySQL的DECIMAL)存储,禁止使用浮点型,避免精度丢失
  • 交易记录的金额字段统一规则:充值类操作存正值,扣款类操作存负值,汇总时直接SUM即可,无需额外判断操作类型
  • 上述所有方案均基于数据库层面实现,天然支持分布式多服务实例部署,不需要额外引入分布式锁组件

内容的提问来源于stack exchange,提问作者Ivan Blohin

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.02 08:27:04