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

金融交易的事务隔离级别选型及悲观锁使用疑问

账户转账事务的隔离级别与锁策略解析

1. 推荐的事务隔离级别

不管是MySQL还是PostgreSQL,READ COMMITTED都是这类转账场景的最优选择:

  • 它能避免脏读,同时比REPEATABLE READ、SERIALIZABLE这类更高隔离级别锁竞争更低,性能更优。
  • 你提到的SERIALIZABLE隔离级别确实存在严重问题:两个并发事务会先获取转出账户的共享锁,更新时需升级为排他锁,但互相持有对方需要的共享锁,必然引发死锁,最终其中一个事务回滚,还会产生数十秒的等待,完全不适合高并发转账场景。

2. 仅依赖隔离级别不够,必须配合显式锁策略

仅靠隔离级别无法保证「余额校验+金额更新」的原子性,会出现两个事务同时读到转出账户余额20,都通过校验后执行更新,最终导致账户A余额变为负数的超卖问题。必须结合显式锁来控制并发,以下分数据库说明具体方案:

MySQL方案

使用READ COMMITTED隔离级别,读取转出账户时显式加排他锁,同时建议统一锁的获取顺序(比如先锁转出账户,再锁转入账户),进一步规避死锁:

START TRANSACTION;
-- 读取账户A并加排他锁,NOWAIT避免长时间等待,MySQL 8.0+也可用SKIP LOCKED直接跳过锁定行
SELECT balance FROM BALANCES WHERE id = 'A' FOR UPDATE NOWAIT;
-- 业务代码中校验余额是否大于转账金额
UPDATE BALANCES SET balance = balance - 10 WHERE id = 'A';
UPDATE BALANCES SET balance = balance + 10 WHERE id = 'B';
COMMIT;

这种方式能保证同一时间只有一个事务能修改账户A的余额,从根源上避免并发校验的错误。

PostgreSQL方案

PostgreSQL的READ COMMITTED隔离级别行为与MySQL一致,同样通过FOR UPDATE NOWAIT加排他锁:

BEGIN;
-- 读取账户A并加排他锁,若锁被持有直接报错,无需等待
SELECT balance FROM BALANCES WHERE id = 'A' FOR UPDATE NOWAIT;
-- 业务代码校验余额
UPDATE BALANCES SET balance = balance - 10 WHERE id = 'A';
UPDATE BALANCES SET balance = balance + 10 WHERE id = 'B';
COMMIT;

PostgreSQL还支持SELECT ... FOR UPDATE SKIP LOCKED,适合不需要等待的高并发场景,可直接跳过锁定行,让业务快速失败重试。

补充:高并发场景可替代的乐观锁方案

如果并发量极高,不想用排他锁导致锁等待,可给BALANCES表新增version字段,用乐观锁控制:

-- 读取时获取余额和版本号
SELECT balance, version FROM BALANCES WHERE id = 'A';
-- 业务代码校验余额
-- 更新时校验版本号,仅当版本匹配时执行更新
UPDATE BALANCES SET balance = balance - 10, version = version + 1 WHERE id = 'A' AND version = [读取到的版本];
-- 若更新影响行数为0,说明版本已变更,事务回滚并重试

这种方案无需加锁,靠版本号冲突控制并发,适合并发量大但冲突概率较低的场景。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 21:50:07