update不直接锁表,但where未命中索引时会全表扫描并逐行加锁,效果等同表锁;有索引时按条件类型和隔离级别加record lock、gap lock或next-key lock。

UPDATE加锁行为完全取决于WHERE条件是否命中索引
没索引的UPDATE会直接升级为表锁,所有行都被锁住,高并发下服务瞬间卡死。有索引才可能走行级锁,但具体锁什么、锁多大范围,还得看查询条件类型和隔离级别。
关键判断逻辑是:InnoDB永远只在索引上加锁,不是在数据行上。主键索引、唯一索引、普通二级索引,加锁行为完全不同。
-
WHERE id = 10(id为主键或唯一索引)→ 只加Record Lock,精确锁定那一行,不涉及间隙 -
WHERE name = 'Alice'(name是普通二级索引)→ 加Next-Key Lock,即(prev_name, 'Alice']区间,包含记录本身和左侧间隙 -
WHERE age > 25(age无索引或非唯一索引)→ 全表扫描,对每个匹配行加Record Lock,同时对所有扫描到的间隙加Gap Lock
RR隔离级别下UPDATE默认用Next-Key Lock
MySQL 8.0+默认事务隔离级别是REPEATABLE READ,这个级别下InnoDB对UPDATE语句的加锁策略是“宁可多锁,不可漏锁”,核心目标是彻底防止幻读。
这意味着哪怕你只更新一行,只要WHERE条件走的是非唯一索引,InnoDB就会自动把该索引值所在的位置扩展成一个左开右闭区间。
- 假设
age列有二级索引,当前存在age=20和age=30两行 → 索引间隙为(20, 30) -
UPDATE users SET status=1 WHERE age = 25→ 因为25不存在,InnoDB会在(20, 30)加Gap Lock -
UPDATE users SET status=1 WHERE age = 20→ 因为20存在,InnoDB加Next-Key Lock,即(prev_age, 20],其中prev_age是前一个索引值
注意:Gap Lock之间完全兼容(多个事务可以同时持有),但Gap Lock与Insert Intention Lock互斥——这就是死锁高频发生点。
RC隔离级别能关掉间隙锁,但要承担幻读风险
把隔离级别改成READ COMMITTED后,UPDATE就只加Record Lock,不再加任何Gap Lock或Next-Key Lock。这是互联网业务线上最常用的折中方案。
- 好处:
UPDATE users SET x=1 WHERE id=100只锁id=100这一行;并发INSERT不会被阻塞 - 代价:同一事务内两次
SELECT ... WHERE id > 50可能返回不同数量的行(幻读) - 限制:必须确保业务逻辑不依赖“范围内无新插入”这个前提,比如库存扣减类场景就不能轻易切RC
改法很简单,在连接初始化时执行:SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;,或者在应用层配置DataSource的defaultTransactionIsolation。
最容易被忽略的坑:UPDATE没走索引却以为只锁一行
开发者常写UPDATE orders SET status='done' WHERE user_id = 123 AND created_at > '2026-09-01',但user_id和created_at各自有单列索引,却没建联合索引 → MySQL优化器可能选错索引,导致全表扫描。
此时即使只更新1条记录,InnoDB也要遍历全表,对每行判断条件,过程中会对所有扫描过的行加Record Lock,并对其间的间隙加Gap Lock。锁范围远超预期,且极易触发死锁。
验证方法只有两个:EXPLAIN看是否用上索引,以及在测试环境用SHOW ENGINE INNODB STATUS\G查LATEST DETECTED DEADLOCK段落里的锁信息。











