mysql update没走索引时锁的是扫描过的所有聚簇索引记录,而非全表本身;因innodb行锁加在索引记录上,无索引则全表扫描并逐行为匹配判断加x锁与间隙锁,rr级别下合为next-key锁,效果等同锁全表。

UPDATE没走索引时,锁的是“扫描过的所有聚簇索引记录”,不是“全表”本身
MySQL InnoDB 并不会因为 WHERE 条件没索引就主动发一个“LOCK TABLE”的指令去锁整张表。它锁的是执行过程中**实际访问并判断过的每一条聚簇索引记录**——也就是全表扫描时 Server 层从引擎层逐条拉出来的那些主键行。这些行被加了 X 锁(记录锁)+ Gap Lock(间隙锁),在 RR 隔离级别下合起来就是 next-key 锁。
所以现象上像“锁全表”,本质是:扫描多少行,就锁多少行。哪怕表有 100 万行,但扫描到第 50 万行时事务中断或崩溃,那前 50 万行的锁依然存在,后 50 万行根本没被触碰,也就没被加锁。
常见错误现象:SHOW ENGINE INNODB STATUS 中 trx_rows_locked 值持续增长,且接近当前已扫描行数;trx_state = RUNNING 但 trx_operation_state = fetching rows。
为什么锁会“逐步释放”,而不是等事务结束才一起放
InnoDB 的行锁释放时机取决于事务状态和语句执行流程,不是统一等到 COMMIT 或 ROLLBACK 才批量释放。关键点在于:
- 如果 UPDATE 是单条语句(无子查询、无触发器),InnoDB 在语句执行结束、返回结果给 Server 层后,**会立即释放未匹配 WHERE 条件的行上的锁**——只保留真正被修改的那几行的 X 锁,直到事务结束
- 但这个“释放”只发生在语句级提交(statement commit)阶段,前提是隔离级别为
READ COMMITTED;而在默认的REPEATABLE READ下,InnoDB 为了防止幻读,会把扫描路径上的所有 next-key 锁都持有到事务结束 - 所谓“逐步释放”,其实是你观察到了多个并发 UPDATE 同一条件但不同批次的行为:比如事务 A 先扫前 10 万行并加锁,事务 B 紧跟着尝试相同条件,被卡在第 1 行就阻塞;此时 A 继续扫到 20 万行,B 还卡着;A 提交后,B 突然能继续往下扫——这不是锁“逐步释放”,而是 B 终于能开始自己的扫描了
EXPLAIN 看不到锁行为,得靠 INNODB_TRX + performance_schema 实时抓
EXPLAIN 只告诉你“计划怎么扫”,不反映“实际锁了哪些行”。想确认是否真在逐步加锁或释放,必须查运行时数据:
- 开两个会话:会话 A 执行
UPDATE t SET x=1 WHERE non_indexed_col = 'val'(不提交),会话 B 立即执行SELECT * FROM information_schema.INNODB_TRX WHERE trx_id = ...查 A 的trx_rows_locked和trx_state - 用
SELECT * FROM performance_schema.data_locks WHERE LOCK_TRX_ID = 'xxx'(MySQL 8.0+)或SHOW ENGINE INNODB STATUS\G(5.7)看具体锁住的SPACE、PAGE、REC编号 - 注意:
trx_rows_locked是累计值,不是当前持有数;它包含已释放但尚未被 purge 线程清理的旧锁信息,所以不能直接等同于阻塞能力
真正影响“锁多久”的,是事务生命周期,不是 SQL 执行时间
很多人以为“UPDATE 执行完就松手了”,其实不然。只要事务没结束,以下情况都会让锁持续挂着:
- 事务中还有后续语句(哪怕只是
SELECT 1),锁不会提前释放 - 开启了
AUTOCOMMIT = 0但没手动COMMIT,锁一直持有着 - 客户端连接断开但事务没超时(
wait_timeout默认 8 小时),锁仍存在,直到连接被 kill 或超时回收 - 使用了
START TRANSACTION WITH CONSISTENT SNAPSHOT,即使只读,也会延长 MVCC 快照窗口,间接拖长锁持有时间
最易被忽略的一点:应用层 ORM(如 Django、MyBatis)常默认开启事务,但开发者未必意识到某次 save() 调用背后绑定了一个长达几秒的事务上下文——这期间所有中间状态都可能成为锁源。











