最有效解法是将隔离级别降为read committed,但必须同步确认binlog_format=row、无唯一索引隐式锁冲突、且应用层“读-判-写”逻辑已改造为原子语句或幂等控制。

MySQL 5.7 默认的 REPEATABLE READ 隔离级别在转账类业务中容易因 Gap Lock 导致并发写入卡顿,尤其当涉及非唯一条件查询或范围更新时;但直接降为 READ COMMITTED 并不总是安全——必须确认 binlog 格式、唯一索引行为和应用层“读-判-写”逻辑是否兼容。
为什么转账业务在 RR 下容易卡住
转账不是简单的一条 UPDATE,往往包含“查余额 → 判定是否足够 → 扣减 → 记流水”多步。在 REPEATABLE READ 下:
- 哪怕你只执行
SELECT balance FROM accounts WHERE user_id = 123 FOR UPDATE,若user_id不是唯一索引,InnoDB 可能加 Next-Key Lock(记录锁 + 间隙锁),锁住整个索引区间 - 更危险的是类似
UPDATE accounts SET balance = balance - 100 WHERE user_id IN (123, 456)—— 若没走索引或走的是普通二级索引,会锁住对应范围的所有间隙,后续插入或更新同一批 user_id 的事务全部排队 - 典型现象:压测时 QPS 上不去、
SHOW ENGINE INNODB STATUS中频繁出现lock_mode X locks gap before rec、应用层报超时但慢查询日志里无慢 SQL
RC 隔离级别下转账能否直接切换
不能无脑切。RC 确实禁用 Gap Lock,只保留记录锁,但以下三点必须验证:
-
binlog_format必须为ROW:执行SHOW VARIABLES LIKE 'binlog_format',若返回MIXED或STATEMENT,RC 下某些UPDATE可能主从不一致,尤其涉及函数或子查询时 - 检查账户表是否有
UNIQUE KEY(如account_no):即使切到 RC,两个事务同时执行INSERT INTO transfers (...) VALUES (...)插入相同唯一值,仍会被隐式间隙锁阻塞 - 应用代码中避免“先
SELECT COUNT(*)再INSERT”这类逻辑:RC 下两次读可能看到不同结果,应改用INSERT ... ON DUPLICATE KEY UPDATE或应用层幂等 ID 控制
如何安全启用 RC 并适配转账逻辑
推荐按连接粒度设置,而非全局修改,便于灰度验证:
- 连接字符串中显式指定:如 JDBC URL 加
?sessionVariables=tx_isolation='READ-COMMITTED' - 或在事务开头执行:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED - 对核心转账语句做最小化改造:把“查→判→写”三步合并为原子语句,例如用
UPDATE accounts SET balance = balance - ? WHERE user_id = ? AND balance >= ?,靠影响行数判断是否扣款成功 - 配套增加监控项:在 Prometheus 中新增指标跟踪
Com_update和Innodb_row_lock_waits,对比切换前后锁等待次数是否下降 80% 以上
真正难的不是改隔离级别,而是确认所有唯一约束、外键依赖和应用层重试逻辑是否与 RC 兼容——尤其是那些隐藏在存储过程或 ORM 自动生成 SQL 中的“读-判-写”分支,它们在 RC 下会暴露幻读风险,却不会报错,只会让资金最终不一致。











