UPDATE 没加索引会锁全表吗?
会——但准确说法不是「上了表锁」,而是 RR 隔离级别下,InnoDB 对全表扫描经过的每一条记录都加了 Next-Key Lock,效果等同锁全表。这是行锁体系里最危险的一种事故,本文还原事故现场、拆解根因、给出完整防御体系。
locks 系列第三篇 · 前两篇讲「锁的种类」与「加锁过程」,本篇聚焦一条经典事故链。
根因:行锁加在索引上
RR 下扫过即锁
RC 有半一致读豁免
治本:给 WHERE 建索引
按提纲四步走,把一个事故题讲成一次复盘。
① 会锁全表吗?
先给结论再抠字眼——「锁全表」三个字里藏着两个常见误解:把它当成「表锁」,以及以为「连 SELECT 也读不了」。
② 为什么会发生?
从「行锁加在索引上」这条第一性原理出发,推演出无索引 → 全表扫描 → 扫过即锁的完整链条。
③ 如何避免?
一套事前 / 事中 / 事后的防御体系:索引、EXPLAIN、安全更新模式、慢日志、SQL 审核、隔离级别选型。
前置知识(前两篇已讲,这里一句话带过):InnoDB 行锁不是锁「数据行」,而是锁「索引记录」;RR 下默认加 Next-Key Lock(记录 + 前面的间隙)。这是理解本篇事故的全部前置。
会,但要抠准「锁全表」的准确含义——三个误解先拆掉。
| 常见说法 | 对不对 | 准确表述 |
| 「UPDATE 没索引会加表锁」 |
不准确 |
InnoDB 加的仍然是行级 Next-Key Lock,只不过被加锁的是每一条记录——是「很多把行锁」,不是「一把表锁」。真正意义上的表锁(LOCK TABLES / MyISAM)是另一回事。 |
| 「锁全表 = SELECT 也读不了」 |
不对 |
普通 SELECT 是快照读(MVCC),完全不受行锁影响,照常能读。被阻塞的只有其它写操作(UPDATE / DELETE / INSERT / 当前读)。 |
| 「RR 和 RC 下行为一样」 |
不一样 |
RR:全表所有记录 + 间隙都被 Next-Key Lock 锁死,直到事务提交。RC:不满足 WHERE 条件的行加锁后立即释放(semi-consistent read),最终只锁住命中的行。详见第 ⑤ 节。 |
所以准确的结论是:在 RR 隔离级别(MySQL 默认)下,一条没走索引的 UPDATE 会让 InnoDB 对全表所有索引记录逐条加上 Next-Key Lock——虽然不是表锁,但阻塞效果等同锁全表:在事务提交前,这张表的所有写操作都会被卡住,只有快照读幸免。
两个会话、一条 SQL,亲眼看「改一行的人堵死全表的人」。
-- 准备:name 字段没有任何索引
CREATE TABLE t_user (
id INT PRIMARY KEY,
name VARCHAR(50) -- 没建索引!
) ENGINE=InnoDB;
-- ════ 会话 A:执行一条没走索引的 UPDATE ════
BEGIN;
UPDATE t_user SET name = 'X' WHERE name = '张三';
-- ════ 会话 B:更新一条完全不相干的行 ════
BEGIN;
UPDATE t_user SET name = 'Y' WHERE id = 25;
-- ↑ 卡住!!明明只改 id=25,和 张三 毫无关系,
-- 却要等 A 提交(或 50 秒锁超时)才能继续。
-- ════ 会话 C:普通 SELECT,不受影响 ════
SELECT * FROM t_user WHERE id = 25;
-- ↑ 正常返回。快照读走 MVCC,不需要锁。
图 1 · 事故时序:A 全表加锁,B 无辜躺枪,C 快照读幸免
雪崩是怎么滚起来的:一条慢 UPDATE 锁全表 → 后续写请求全部等锁 → 每个等锁请求占用一个数据库连接 → 连接池耗尽 → 新请求连库都连不上 → 应用线程同步阻塞 → 上游服务线程池打满 → 整个调用链雪崩。一次「WHERE 条件没走索引」可以掀翻一组服务——这就是事故的本质。
从第一性原理推:三个环节缺一不可。
图 2 · 事故根因链:每一环都是「设计如此」,连起来就是灾难
对比看透本质:同一张表,索引决定锁的范围
同样的 UPDATE ... WHERE name='张三',给 name 建上索引之后,锁的范围从「全表」缩到「一行 + 一个间隙」:
图 3 · 有索引 vs 无索引:锁范围天差地别(索引值分布 1,5,10,15,20,25)
我的理解——这不是 MySQL 的 bug,而是「职责所在」:InnoDB 锁索引记录,是因为索引才是「定位一行」的唯一手段。没有索引时,MySQL 根本无法知道「哪些行会满足 WHERE」,为了不出错(防幻读、防漏锁),只能宁可错杀不可放过——把扫过的都锁住。所以事故的锅不在 InnoDB,而在「让 InnoDB 盲扫的 SQL」。理解了这一点,防御方案就自然浮出来了:要么给它索引,要么拦住这种 SQL 上线。
半一致读(semi-consistent read)是 RC 的隐藏福利。
同样一条无索引 UPDATE,在 RC 下只锁「最终满足 WHERE 的行」。原因是 RC 下 InnoDB 采用了半一致读策略:
图 4 · RC 的半一致读:先「试读」不锁,确定命中才真正加锁
延伸一句:这正是很多互联网公司把隔离级别从 RR 改成 RC 的又一理由——除了缩小间隙锁范围,半一致读还让「无索引 / 索引选择性差」的 UPDATE 不再成为全表锁事故。当然,RC 需要配合 ROW 格式的 binlog,且放弃了幻读保护。
单点手段都靠不住,要织一张「事前 → 事中 → 事后」的网。
| 时机 | 手段 | 怎么做 |
事前 (设计期) |
治本 给 WHERE 字段建索引 |
排查所有高频 UPDATE / DELETE 的 WHERE 列,逐个确认有索引。这是唯一能根治的手段。 |
| 推荐 隔离级别评估 |
高并发写入、可接受幻读的业务,评估改用 RC:间隙锁消失 + 半一致读兜底,同类事故自然免疫大半。 |
| 兜底 大事务拆小 |
批量 UPDATE 分批提交(如每 1000 行一批),即使误锁全表,持锁时间也被限制在毫秒级。 |
事前 (上线前) |
必做 EXPLAIN 检查 |
每条上线的 UPDATE / DELETE 先 EXPLAIN,type=ALL 且表很大 → 直接打回。也可在 SQL 审核平台里强制卡点。 |
| 必做 SQL 审核卡点 |
把「UPDATE/DELETE 必须走索引」写进上线流程(人工评审或自动化审核),不靠个人自觉。 |
| 一行配置 安全更新模式 |
SET GLOBAL sql_safe_updates=1; 之后不带索引条件的 UPDATE/DELETE 直接报错(除非带 LIMIT)。开发环境强烈建议开启。 |
事中 (运行期) |
慢日志兜底 |
开启 log_queries_not_using_indexes,未走索引的 SQL 全部记入慢日志,主动发现而不是等事故。 |
| 监控锁等待 |
盯 sys.innodb_lock_waits 与连接池水位;「写请求突然全部变慢」往往就是全表锁的第一症状。 |
| 事后 |
应急 + 复盘 |
应急:KILL 阻塞源(先查 innodb_trx 找到未提交的长事务)。复盘:为什么没索引的 SQL 上得了线?补流程。 |
-- 安全更新模式:没有索引条件的 UPDATE/DELETE 直接报错
SET GLOBAL sql_safe_updates = 1;
UPDATE t_user SET name='X' WHERE name='张三';
-- ERROR 1175 (HY000): You are using safe update mode ...
-- (name 无索引且未带 LIMIT → 直接拒绝)
-- 慢日志:把没走索引的 SQL 全抓出来
SET GLOBAL log_queries_not_using_indexes = ON;
SET GLOBAL long_query_time = 0.5;
模拟 sql_safe_updates 的判定逻辑 + 事故风险等级,选条件看结论。
WHERE 条件
带索引列
带非索引列
没有 WHERE
LIMIT 限制
无
有 LIMIT
隔离级别
可重复读 RR
读已提交 RC
安全:只锁命中行
可控:锁范围可评估
危险:等效锁全表
一句话收尾
- 会锁全表吗?RR 下会——不是表锁,而是全表每条记录都被加了 Next-Key Lock,阻塞效果等同表锁;普通 SELECT 因快照读不受影响。
- 为什么发生?行锁加在索引上 → 无索引只能全表扫描 → RR 下扫过即锁(防幻读宁可错杀)→ 写操作全部排队 → 连接池耗尽 → 雪崩。
- RC 为什么没事?半一致读让「不满足条件的行」锁后立即释放,最终只锁命中行。
- 怎么避免?治本一条:WHERE 字段建索引;流程两条:EXPLAIN 上线卡点 + sql_safe_updates;兜底三条:慢日志、批量拆分、评估改 RC。
速记口诀
- 行锁锁在索引上,没索引就锁全表
- RR 扫过即锁到提交,RC 试完即放只锁命中
- 快照读不怕,怕的是写
- 上线先 EXPLAIN,type=ALL 必打回
三个易错点
- 「锁全表」≠ 表锁——是很多把行锁,别和 MyISAM 表锁混淆
- 别以为只影响「这张表的写」——连接池被占满会殃及全应用
- 别以为小表没事——业务量涨了、扫过的行多了,同样的 SQL 迟早炸
关联知识
- 为什么扫过即锁:见本系列第二篇「万能加锁规则」
- Next-Key / Gap 是什么:见第一篇速查手册
- RC 与 RR 的取舍:隔离级别专题