MySQL的UPDATE默认行级锁,但WHERE条件需命中索引;若索引失效导致全表扫描,则锁所有扫描行;应通过EXPLAIN检查key和rows,避免对索引列运算。

UPDATE 语句默认不锁全表,但是否加行锁取决于 WHERE 条件和索引使用情况
MySQL 的 UPDATE 在 InnoDB 引擎下默认走行级锁,**前提是 WHERE 条件能命中索引**。如果 WHERE 没有可用索引(或用了函数、隐式类型转换导致索引失效),优化器会退化为全表扫描,此时 InnoDB 会对所有扫描过的记录加锁——看起来像“锁全表”,实际是锁了所有被扫描的行,包括最后被 UPDATE 的目标行和中途判断不满足条件的行。
常见错误现象:UPDATE users SET status = 1 WHERE phone = '138xxxx' 执行极慢且阻塞其他事务,查 EXPLAIN 发现 type=ALL,说明 phone 列没建索引或用了 LIKE '%xxx' 导致索引失效。
- 检查索引是否生效:用
EXPLAIN FORMAT=TRADITIONAL看key和rows字段 - 避免在 WHERE 中对索引列做运算,如
WHERE YEAR(create_time) = 2024→ 改为WHERE create_time >= '2024-01-01' AND create_time - 字符串比较注意字符集和校对规则,
utf8mb4_bin和utf8mb4_0900_as_cs不兼容时可能触发隐式转换
InnoDB 执行 UPDATE 时的锁申请流程:从聚簇索引查找开始
InnoDB 不直接操作数据页,而是通过聚簇索引定位记录。执行 UPDATE 时,执行器会调用存储引擎接口,按以下顺序申请锁:
- 先对
WHERE条件扫描路径上的每条记录(包括不满足条件的)加next-key lock(间隙锁 + 记录锁),防止幻读和并发修改干扰判断 - 对最终匹配成功的记录,在更新前升级为
X(排他)记录锁;若涉及二级索引更新,还会对二级索引记录加X锁 - 如果
UPDATE导致主键值变更(例如UPDATE t SET id = id + 1 WHERE id = 100),旧主键记录加X锁,新主键位置也要加X锁(并检查唯一约束)
典型陷阱:UPDATE orders SET paid = 1 WHERE user_id = 123 AND status = 'unpaid',若 (user_id, status) 没有联合索引,可能扫描大量 user_id = 123 的记录,全部加锁,而不仅是 status = 'unpaid' 的那几行。
UPDATE 语句是否会升级为表锁?只有两种明确场景
InnoDB 在绝大多数情况下不会主动升级为表锁,但以下两种情况会导致整张表不可并发写入(注意:不是传统意义的“表锁”,而是因锁冲突导致的逻辑阻塞):
-
ALTER TABLE或DROP INDEX等 DDL 正在执行时,后续UPDATE会被阻塞在元数据锁(MDL)上,表现为Waiting for table metadata lock - 事务中先执行了
SELECT ... FOR UPDATE未命中索引,锁住大量记录;再执行UPDATE时因锁等待超时(Lock wait timeout exceeded)或死锁被回滚,看起来像“锁表”
注意:LOCK TABLES WRITE 是显式表锁,与 InnoDB 行锁机制无关,且在事务中混用会导致隐式提交,极易出错,生产环境应禁用。
验证当前 UPDATE 持有哪些锁:用 information_schema 分析
当怀疑锁行为异常时,别只看 SHOW PROCESSLIST,要查真实锁状态:
- 查阻塞关系:
SELECT * FROM performance_schema.data_lock_waits(MySQL 8.0+)或SELECT * FROM information_schema.INNODB_TRX+INNODB_LOCKS+INNODB_LOCK_WAITS(5.7) - 确认锁范围:
SELECT * FROM performance_schema.data_locks WHERE OBJECT_NAME = 'your_table',重点关注LOCK_MODE(如X,REC_NOT_GAP是纯记录锁,X,GAP是间隙锁)和LOCK_DATA(显示锁定的索引值) - 特别注意
LOCK_TRX_ID对应的事务是否已提交或长时间空闲,未提交事务持有的锁不会释放
容易被忽略的一点:即使 UPDATE 语句本身很快,只要它所属的事务没提交,它申请的所有锁就一直持有——所以长事务比慢 SQL 更危险。











