行锁会在索引失效时退化为表锁,如LIKE左模糊、隐式类型转换、函数操作字段等导致全表扫描;此时InnoDB加意向锁并实际锁全表。
MySQL 的 InnoDB 默认用行锁,但不是所有带索引的 WHERE 条件都能命中行锁。一旦优化器判定无法使用索引、或用了不走索引的条件(比如 LIKE '%abc'、隐式类型转换、函数包裹字段),就会触发全表扫描,进而升级为表级意向锁 + 实际的表锁行为。
常见退化场景:
UPDATE user SET status=1 WHERE name LIKE '%admin%' —— name 有索引但 LIKE 左模糊导致索引失效SELECT * FROM order WHERE user_id = '123',而 user_id 是 INT 类型,传入字符串 '123' 触发隐式转换,索引失效SELECT ... FOR UPDATE,但查询条件没走索引,InnoDB 会对所有聚集索引记录加锁,效果等同于锁表这个语句本质是「先尝试插入,冲突则更新」,InnoDB 对它做了专门优化:只对将要插入/更新的那几行加 next-key lock(间隙锁 + 行锁),不会锁整张表。
但要注意前提:
PRIMARY KEY 或 UNIQUE KEY,否则冲突判断无依据,语句会报错,更谈不上加什么锁record lock;如果插入位置存在间隙,还会额外加 gap lock
UPDATE 部分修改了唯一索引字
段本身(比如 UPDATE id = id + 1),可能导致锁范围扩大,甚至触发死锁SELECT ... FOR UPDATE 是否锁住多行、是否锁间隙,完全取决于你的 WHERE 条件是否能走索引,以及索引类型。
实操建议:
WHERE id = 100),锁的是单条记录的 record lock
WHERE status = 1),InnoDB 会加 next-key lock,即锁住匹配记录 + 该记录前的间隙,防止幻读WHERE created_at > '2025-01-01')一定包含间隙锁,范围越大,锁越多,容易阻塞其他事务READ COMMITTED(需确认业务能否接受不可重复读)这是诊断锁问题最直接的命令,输出里 TRANSACTIONS 部分会列出当前阻塞关系和持锁情况。重点关注:
Trx id 和 Trx mysql thread id:定位具体哪个线程卡住了Trx state: LOCK WAIT 表示正在等锁;Trx state: RUNNING 不代表没锁,只是当前没在等lock_mode X locks rec but not gap:说明是纯行锁;lock_mode X locks gap before rec 是间隙锁;lock_mode X 单独出现大概率是表锁或意向锁Record lock, heap no 123 中的 heap no 是聚簇索引堆记录号,结合 SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX 可交叉验证mysql> SHOW ENGINE INNODB STATUS\G ... ---TRANSACTION 42156789, ACTIVE 12 sec starting index read mysql tables in use 1, locked 1 LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s) MySQL thread id 123, OS thread handle 140234567890176, query id 456789 localhost root updating UPDATE account SET balance = balance - 100 WHERE id = 5
上面这段输出里,“locked 1” 是指锁了一张表(逻辑),但实际是这张表里的某一行被 X 锁住了——粒度得看后面的 row lock(s) 数量和类型,不能只看 locked 1 就认为是表锁。
锁粒度不是写死的,它由查询路径、索引结构、隔离级别共同决定。很多人调优时只改 SQL 写法,却忘了检查执行计划是否真走了索引,或者忽略了 autocommit=0 下事务长期不提交导致锁一直挂着——这才是并发下锁问题最常被忽略的根因。