为什么UPDATE比SELECT危险得多?锁范围等于扫描行数

大家好,我是小耶,写功课只是为了我踩过的坑,你们别再踩了!

先问一个问题。

一条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 不愁

还有什么想了解的,欢迎留言!小耶一定知无不言言无不尽......我们下次见~

相关推荐
天国梦1 小时前
国密门禁品牌怎么选?2026年最新3个避坑要点
运维·服务器·数据库·python
DB哥讲数据库1 小时前
【达梦数据库安装教程】超详细图文讲解-附达梦安装包
数据库·oracle
沈麽鬼2 小时前
纸上代码
代码规范
Hoxy.R2 小时前
【金仓数据库产品体验官】KingbaseES V9R2C16 坏块检测与修复完整测试流程
数据库
我不是阵雨2 小时前
分布式事务难题与实战解法全解析
分布式·微服务·架构
尤水就下2 小时前
🔥预热:在线文档的 block 架构是如何实现的(未完待续...)
前端·javascript·架构
Dawson Zhu3 小时前
《Agentic Design Patterns》第 13 章导读:人机协同(Human-in-the-Loop)
人工智能·语言模型·架构·aigc·agi
姜鱼问生3 小时前
fail2ban 实战:从默认配置到生产可用
后端·算法·架构
太理摆烂哥4 小时前
MySQL数据库基础
数据库·mysql