read committed是唯一能实质性关闭间隙锁的隔离级别,需全局设置、配合row格式binlog,并接受不可重复读;唯一索引冲突和外键引用时仍会触发间隙锁。

READ COMMITTED 是唯一能实质性关闭间隙锁的隔离级别
MySQL 中间隙锁(Gap Lock)不是靠开关配置关闭的,而是由事务启动时的全局隔离级别决定的行为。只有 READ COMMITTED 能让 InnoDB 在绝大多数场景下跳过间隙锁,只对实际命中的行加记录锁(Record Lock)。它不是“禁用功能”,而是隔离语义本身不依赖间隙锁防幻读——所以自然不加。
关键点在于:这不是会话级设置能生效的事。执行 SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED 没用,必须:
-
SET GLOBAL tx_isolation = 'READ-COMMITTED'(注意是连字符写法) - 重启所有已存在的客户端连接(旧连接仍按原隔离级别运行)
- 确认生效:
SELECT @@global.tx_isolation返回READ-COMMITTED
必须同步切换 binlog_format 为 ROW
如果 binlog 还是 STATEMENT 或 MIXED,主从会不一致。原因很直接:READ COMMITTED 下同一条 SQL 在不同事务中可能看到不同快照(不可重复读),而 STATEMENT 格式只记录 SQL 文本,从库重放时无法还原这种非确定性行为。
检查与修改:
- 查当前格式:
SHOW VARIABLES LIKE 'binlog_format' - 临时改(重启后失效):
SET GLOBAL binlog_format = 'ROW' - 持久化:在
my.cnf中写入binlog_format = ROW并重启 MySQL
漏掉这步,上线后可能突然出现主从延迟飙升、数据错位,且日志里查不到明显报错。
唯一索引冲突和外键引用时间隙锁仍存在
别以为设成 READ COMMITTED 就一劳永逸。InnoDB 会在两类底层一致性保障场景中“破例”加间隙锁:
- 插入或更新违反唯一约束(主键/唯一索引)时:需锁定冲突值周边间隙,防止其他事务插入相同值。例如
INSERT INTO t(id) VALUES(50),但表中最大 id 是 49,InnoDB 仍会锁(49, +∞) - 存在外键引用时:父表上相关索引范围可能被加间隙锁,以保证参照完整性
这意味着你关掉的只是业务层常规 UPDATE/DELETE 的间隙锁,不是所有间隙锁。死锁日志里突然冒出 waiting for gap lock,往往就出在这两处。
应用层必须接受不可重复读并重新校验逻辑
关间隙锁换来的代价是:同一事务内多次 SELECT 可能返回不同结果。比如订单服务先查库存 > 10,再扣减,中间被其他事务插入新订单导致库存变少——这不是 bug,是 READ COMMITTED 的合法行为。
应对方式不是回退隔离级别,而是重构逻辑:
- 把“读-改”拆成原子操作:用
UPDATE ... WHERE stock > 10直接判断并扣减,靠影响行数判断是否成功 - 必要时加应用层分布式锁,但仅限极低频关键路径
- 避免在事务内做跨多条 SQL 的业务判断,尤其涉及计数、范围统计
最常被忽略的是:MyBatis-Plus 的 saveOrUpdate(entity, wrapper) 方法先 UPDATE 后 INSERT,在 READ COMMITTED 下虽不加间隙锁,但并发调用仍可能因唯一索引冲突触发隐式间隙锁,进而引发死锁——得看死锁日志里是不是有 duplicate key 和 gap lock 同时出现。











