大家好,我是小耶,写功课只是为了我踩过的坑,你们别再踩了!
先问一个问题。
一条UPDATE语句,WHERE条件精确匹配了10行,最终也只改了10行。你觉得它会锁多少行?
10行?
不对。锁的范围等于扫描的行数,不是最终修改的行数。
如果WHERE条件没走索引,InnoDB会全表扫描。扫过的每一行,都会被加上锁。扫描了100万行,就锁了100万行------即使最终只改了10行。
这就是所谓的"锁升级"。但严格来说,InnoDB根本没有"锁升级"这个机制。它不会主动把行锁升级为表锁。你看到的"等效表锁",是WHERE条件没走索引导致的自然结果。
今天把这个反直觉的结论彻底拆开讲清楚。
先搞懂几个词:
记录锁(Record Lock) :锁住索引记录本身。
间隙锁(Gap Lock) :锁住索引记录之间的间隙,防止其他事务在间隙中插入数据。RR隔离级别下默认启用。
临键锁(Next-Key Lock) :记录锁+间隙锁的组合,InnoDB默认的锁模式。
扫描行数:InnoDB在执行UPDATE/DELETE时,通过索引或全表扫描定位目标行的过程中实际读取的行数。
一、为什么UPDATE/DELETE比SELECT危险得多
SELECT和UPDATE/DELETE在锁行为上有本质区别。
SELECT是"看一眼就走"------找到符合条件的行,读取数据,返回结果,不加锁(快照读)或加短暂的共享锁(当前读)。
UPDATE/DELETE是"找到了还要动手"------找到符合条件的行之后,需要修改数据,必须加排他锁。而且这个锁要一直持有到事务提交。
更关键的是:UPDATE/DELETE加锁的范围不是"最终修改的行",而是"扫描过程中触及的所有行"。
二、锁升级的三种典型场景
场景一:WHERE条件列无索引
bash
-- status列没有索引
UPDATE orders SET status = 'CANCELLED' WHERE status = 'PENDING';
如果status列没有索引,InnoDB只能全表扫描。扫描过程中,聚簇索引上的每一行都会被加上临键锁。如果表有500万行,等于锁了500万行。其他事务对这500万行中任何一行的修改都会被阻塞。
场景二:WHERE条件选择性极低
bash
-- status='DONE'占全表的90%
UPDATE orders SET remark = 'processed' WHERE status = 'DONE';
即使status列有索引,如果status='DONE'占了全表的90%(450万行),优化器可能判断"走索引还不如全表扫描"。于是走全表扫描,锁住所有扫描过的行。
场景三:RR隔离级别下的间隙锁累积
RR隔离级别下,InnoDB不仅加记录锁,还会加间隙锁。全表扫描时,每两行之间的间隙都会被锁住。这意味着其他事务不仅不能修改已存在的行,也不能插入新行。
如果事务长时间不提交,这些间隙锁会一直持有,导致其他事务的INSERT操作被阻塞。
三、怎么确认锁的范围
方法一:查看当前锁等待
bash
SELECT * FROM sys.innodb_lock_waits\G
如果有输出,说明当前有事务在等锁。看blocking_query字段,能直接定位是哪条SQL在阻塞别人。
方法二:查看InnoDB状态
bash
SHOW ENGINE INNODB STATUS\G
在TRANSACTIONS段中,可以看到当前活跃事务持有的锁信息,包括锁的类型(记录锁、间隙锁、临键锁)和锁定的索引范围。
方法三:用EXPLAIN确认扫描行数
bash
EXPLAIN UPDATE orders SET status = 'CANCELLED' WHERE status = 'PENDING';
关注type列和rows列。如果type=ALL(全表扫描)且rows很大,锁的范围就会很大。
四、解决方案
方案一:游标分批
不要一次性UPDATE/DELETE大量数据,而是分批执行。每批处理1000行,提交后再处理下一批。
bash
-- 伪代码
WHILE (1=1) DO
UPDATE orders SET status = 'CANCELLED'
WHERE status = 'PENDING' LIMIT 1000;
IF ROW_COUNT() = 0 THEN LEAVE;
COMMIT;
END WHILE;
方案二:索引优化
让WHERE条件走索引,减少扫描行数。这是最根本的解决方案。
bash
-- 在status列上建索引
CREATE INDEX idx_status ON orders(status);
方案三:RC隔离级别+ROW格式binlog
如果业务允许,将隔离级别从RR改为RC。RC下没有间隙锁,锁的范围只限于匹配的行,不会锁住行之间的间隙。
同时确保binlog_format=ROW,这是RC下减少锁争用的前提条件。
方案四:用pt-archiver工具
Percona Toolkit的pt-archiver专门用于分批归档和删除数据。它会自动分批、自动提交、自动控制速度,避免大事务长时间持锁。
五、小结
InnoDB没有"锁升级"机制。"锁升级"是WHERE条件没走索引导致全表扫描、扫描过的每一行都被加锁的自然结果。锁的范围等于扫描行数,不是修改行数。避免锁升级的核心是让WHERE条件走索引,同时控制事务大小、缩短锁持有时间。RR隔离级别下间隙锁会加剧锁的范围,如果业务允许,考虑用RC隔离级别。
小耶在手,SQL 不愁
还有什么想了解的,欢迎留言!小耶一定知无不言言无不尽......我们下次见~