UPDATE会锁整张表是因为WHERE条件未命中索引,导致InnoDB退化为表级锁;正确使用索引、避免隐式转换、优化事务粒度可避免该问题。

为什么 UPDATE 会锁整张表而不是只锁行
MySQL 的 InnoDB 引擎默认走行级锁,但前提是必须命中索引。如果 UPDATE 或 DELETE 语句的 WHERE 条件没走索引(比如用了函数、隐式类型转换、或字段无索引),InnoDB 就会退化为锁全表——这时并发写入直接排队,看着像卡死。
实操建议:
- 用
EXPLAIN看执行计划,确认type是ref/range而非ALL,且key列显示实际使用的索引 - 避免在
WHERE里对索引列做运算:WHERE YEAR(create_time) = 2024→ 改成WHERE create_time >= '2024-01-01' AND create_time - 检查字段类型是否一致:比如
user_id是BIGINT,但传了字符串'123',会触发隐式转换导致索引失效
SELECT ... FOR UPDATE 锁范围比你想的更大
很多人以为加了主键条件就只锁那一行,其实 InnoDB 的间隙锁(Gap Lock)和临键锁(Next-Key Lock)会让锁覆盖“值之间的空隙”。比如表里有 id 值 1、5、10,执行 SELECT * FROM t WHERE id > 3 FOR UPDATE,可能锁住 (3,5)、(5,10)、(10,+∞) 这几个间隙,阻塞其他事务插入 4、6、11 等值。
实操建议:
- 业务允许时,把
SELECT ... FOR UPDATE换成UPDATE ... WHERE原子操作,减少锁持有时间 - 确认隔离级别:如果业务能接受可重复读以外的一致性,可设为
READ COMMITTED,它不启用间隙锁(但要注意幻读风险) - 高并发更新单条记录时,优先用带主键等值条件的
UPDATE,避免范围条件
事务太长是并发杀手,不是锁粒度问题
锁粒度再小,如果事务从开连接、查数据、算逻辑、再更新,耗时 200ms,那这 200ms 内锁就一直占着。很多“锁竞争”本质是事务臃肿,不是 SQL 没优化。
实操建议:
- 把非数据库操作(比如调外部 API、格式化字符串)全挪到事务外,
BEGIN和COMMIT之间只留必要 SQL - 批量写入别用循环单条
INSERT,改用INSERT INTO ... VALUES (...), (...), (...)一条语句搞定 - 查+改场景慎用“先查后更”:如果只是计数器自增,直接
UPDATE t SET cnt = cnt + 1 WHERE id = ?更安全
唯一索引冲突会引发额外的插入意向锁
当多个事务同时 INSERT 同一个唯一键(比如重复注册邮箱),即使还没提交,InnoDB 也会加插入意向锁(Insert Intention Lock),它和间隙锁冲突,导致事务互相等待——现象是 SHOW ENGINE INNODB STATUS 里看到 lock_mode X insert intention waiting。
实操建议:
- 应用层提前校验:插入前先
SELECT一次(不加锁),命中则直接返回,避免撞唯一索引 - 用
INSERT IGNORE或ON DUPLICATE KEY UPDATE替代普通INSERT,失败不抛异常,减少事务回滚开销 - 如果业务允许,把唯一约束从数据库下推到应用缓存(如 Redis
SETNX)做第一道过滤
锁竞争真正的复杂点不在“怎么加锁”,而在于“锁什么时候释放”和“锁住了什么”。很多问题表面看是并发低,实际是某条慢查询拖住了整个事务,或者一个没注意的全表扫描让行锁变表锁。盯住 INFORMATION_SCHEMA.INNODB_TRX 和 INNODB_LOCK_WAITS,比盲目调参数有用得多。


















