在使用MySQL InnoDB引擎开发业务时,间隙锁是一个容易被忽略但又影响很大的机制,不少开发者都遇到过事务莫名阻塞、并发量上不去的问题,追根溯源往往和间隙锁有关。下面我们就一步步搞清楚如何避免间隙锁带来的负面影响。

什么是InnoDB间隙锁
InnoDB的间隙锁(Gap Lock)是锁定索引记录之间的间隙,或者锁定第一个索引记录之前、最后一个索引记录之后的间隙的锁。它只在可重复读(RR)隔离级别下生效,主要作用是防止其他事务在锁定的间隙中插入新记录,从而避免幻读问题。
比如表中存在id为1、5、10的三条记录,当我们执行SELECT * FROM test WHERE id > 5 AND id < 10 FOR UPDATE时,不仅会锁定id=10这条记录,还会锁定(5,10)这个间隙,其他事务无法在这个区间插入id为6、7、8、9的记录。
哪些操作会触发间隙锁
以下几类常见操作很容易触发间隙锁,需要特别注意:
- 范围查询加锁:使用
FOR UPDATE或者LOCK IN SHARE MODE的范围查询,都会触发间隙锁。 - 更新不存在的记录:比如执行
UPDATE test SET name='a' WHERE id=6,如果id=6的记录不存在,会锁定(5,10)这个间隙。 - 非唯一索引的范围操作:如果查询条件使用的是非唯一索引,间隙锁的范围会更大,锁冲突概率更高。
避免间隙锁的实用方案
1. 降低事务隔离级别
如果业务不需要可重复读的隔离级别,可以将隔离级别调整为读已提交(RC)。在RC级别下,InnoDB不会使用间隙锁,能大幅减少锁冲突。修改方式如下:
-- 查看当前隔离级别 SELECT @@transaction_isolation; -- 设置当前会话的隔离级别为读已提交 SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; -- 全局设置需要修改配置文件,在my.cnf中添加 [mysqld] transaction-isolation = READ-COMMITTED
需要注意,RC级别下无法避免幻读,需要评估业务是否可以接受这个特性。
2. 缩小加锁范围
尽量避免使用大范围的条件加锁,比如不要把WHERE id > 100这类范围条件直接用在加锁查询里。如果只需要处理某几条记录,尽量用精确的主键或者唯一索引条件查询加锁,比如SELECT * FROM test WHERE id IN (1,2,3) FOR UPDATE,这种情况只会锁定对应的记录,不会触发间隙锁。
3. 缩短事务持有锁的时间
把事务中不必要的操作移到事务外面,尽量让加锁的操作放在事务的最后执行,减少锁的持有时间。比如下面的错误写法和优化写法对比:
-- 错误写法:先加锁,再做其他无关操作,锁持有时间长 START TRANSACTION; SELECT * FROM test WHERE id=1 FOR UPDATE; -- 这里做了很多和本次更新无关的业务逻辑处理 UPDATE test SET name='new' WHERE id=1; COMMIT; -- 优化写法:先处理无关逻辑,最后再加锁更新 START TRANSACTION; -- 先处理其他业务逻辑 SELECT * FROM test WHERE id=1 FOR UPDATE; UPDATE test SET name='new' WHERE id=1; COMMIT;
4. 避免非唯一索引的范围加锁
如果查询条件用的是非唯一索引,尽量转换成主键或者唯一索引的精确查询。比如表中有一个非唯一的status索引,要更新status=1的记录,不要直接写UPDATE test SET name='a' WHERE status=1,可以先查到对应的主键id,再用主键id去更新:
-- 先查询主键,缩小加锁范围 SELECT id FROM test WHERE status=1; -- 假设得到id为1,2,3,再用主键更新 UPDATE test SET name='a' WHERE id IN (1,2,3);
注意事项
间隙锁本身是InnoDB用来保证数据一致性的机制,不要为了完全避免间隙锁而牺牲数据正确性。如果业务场景必须保证可重复读和幻读防护,还是要合理使用间隙锁,只是要尽量避免不必要的间隙锁触发,平衡好一致性和性能的关系。
需要注意的是,即使使用了上述方案,在RR隔离级别下,只要涉及范围加锁还是会触发间隙锁,这时候需要结合业务场景评估是否真的需要RR级别。