mysql行锁变表锁的真相是:无索引时innodb被迫全表扫描并对所有主键记录加行锁,等效锁表;隐式转换、字符集不一致等也会导致索引失效而触发该行为。

非索引字段触发行锁变表锁的真相
MySQL 的 SELECT ... FOR UPDATE 或 UPDATE 语句,**不是“有 WHERE 就锁行”**,而是“有可用索引才锁行”。一旦 WHERE 条件里出现没建索引的字段(比如 created_at > '2024-01-01' 但 created_at 无索引),InnoDB 就必须全表扫描——所有被扫描过的主键记录都会被加上记录锁,等效于锁全表。
更隐蔽的是隐式转换:WHERE user_id = '123'(user_id 是 INT)会强制类型转换,导致索引失效;同理,utf8mb4 列和 utf8 字符串比较也会跳过索引。这些情况都不会报错,但锁范围会无声扩大。
快速定位哪些 SQL 正在锁全表
别靠经验猜,直接查真实执行路径:
- 开启全量慢日志:
SET GLOBAL slow_query_log = ON,并设long_query_time = 0+log_queries_not_using_indexes = ON - 查高影响写操作:
SELECT * FROM performance_schema.events_statements_history WHERE sql_text LIKE '%UPDATE%' AND rows_affected > 1000 - 对可疑语句跑
EXPLAIN FORMAT=JSON,重点看:
–key字段是否为NULL
–rows是否远超业务预期(比如查 10 条却扫 50 万行)
–filtered是否极低(表示条件过滤效率极差)
加联合索引时必须满足最左前缀原则
单列索引不够,多条件查询得靠联合索引,但顺序错了照样失效。例如查询 WHERE status = 'paid' AND created_at > '2024-01-01':
- ✅ 有效索引:
INDEX idx_status_created (status, created_at)——status在前,可走索引范围扫描 - ❌ 无效索引:
INDEX idx_created_status (created_at, status)——created_at是范围条件,status后续无法利用索引 - ⚠️ 注意:如果还带
ORDER BY created_at DESC,则索引需包含排序方向,否则可能放弃索引使用 filesort
KILL 并不能解决根本问题
用 SHOW PROCESSLIST 找到 State: Locked 或长时间 Updating 的线程,再 KILL [id] 只是临时止血。真正的问题在下一次相同 SQL 执行时立刻复现。
关键要确认两点:
– 这条 SQL 是否真的需要锁这么多行?能不能改用 SELECT ... LOCK IN SHARE MODE 或去掉 FOR UPDATE?
– 对应的 WHERE 字段有没有被漏掉建索引?有没有在应用层做了字符串拼接导致参数无法走索引?
锁升级不是 MySQL 的 bug,是它在没有索引时唯一能保证事务一致性的手段。你看到的“卡住”,其实是引擎在认真干活——只是干的活比你预想的多得多。











