innodb行锁实际通过索引项加锁,未命中索引时会升级为表锁;唯一索引等值查询只锁一行,非唯一索引可能锁多行及间隙;无记录时间隙锁防幻读;覆盖索引可减少锁范围。

索引没走对,行锁就变表锁
InnoDB 的行锁不是“按行号”锁的,而是通过索引项加锁——只有当 WHERE 条件能命中索引(且引擎层能直接过滤掉无关行),才真正只锁命中的那几行。否则,InnoDB 会先锁住所有扫描过的行(甚至退化为全表扫描+全表加锁)。
- 常见错误现象:
UPDATE orders SET status='done' WHERE user_id = 123执行极慢、阻塞大量其他事务,EXPLAIN显示type=ALL(全表扫描) - 根本原因:
user_id没建索引,或建了但类型不匹配(如字段是VARCHAR,却用数字123查询,触发隐式转换导致索引失效) - 实操建议:对所有
WHERE、JOIN、ORDER BY字段建立合适索引;用EXPLAIN FORMAT=TRADITIONAL确认key列是否非NULL,rows是否显著小于表总行数
唯一索引等值查询只锁一行,非唯一索引可能锁多行
锁的范围和索引类型强相关——不是“有没有索引”,而是“什么索引 + 什么查询条件”。MySQL 会根据索引结构和隔离级别动态选择锁粒度。
- 场景对比:
—SELECT * FROM users WHERE id = 10 FOR UPDATE(id是主键)→ 只锁id=10这一行
—SELECT * FROM users WHERE name = 'Alice' FOR UPDATE(name是普通二级索引,且有多个 'Alice')→ 锁所有匹配的索引记录,以及这些记录之间的间隙(即临键锁) - 容易踩的坑:以为加了索引就“安全”,结果因索引非唯一+高并发更新相同值,引发大量锁等待甚至死锁
- 优化方向:对高频更新的查询字段,优先使用唯一索引;若必须用非唯一索引,考虑在应用层加分布式锁或业务层限流,避免多个事务同时争抢同一索引值
没查到数据时,间隙锁可能“悄悄锁住一片”
这是最反直觉也最容易被忽略的一点:当唯一索引等值查询未命中记录,InnoDB 不会什么都不锁,而是加一个**间隙锁**(Gap Lock),防止其他事务在该间隙插入新记录——目的是保证可重复读(RR)隔离级别下的幻读一致性。
- 典型表现:
SELECT * FROM orders WHERE order_no = 'NO20260128001' FOR UPDATE返回空,但后续INSERT INTO orders (order_no, ...) VALUES ('NO20260128001', ...)被阻塞 - 为什么这样设计:防止在事务 A 判断“此单号不存在”后,事务 B 插入同单号,导致 A 再次查询时出现幻读
- 注意点:间隙锁不冲突于纯读操作(如
SELECT ... LOCK IN SHARE MODE不会阻塞 SELECT),但会阻塞 INSERT;若业务允许读已提交(RC)隔离级别,可降级以彻底关闭间隙锁(但需承担幻读风险)
覆盖索引能减少锁范围,还能避免回表带来的额外锁
当查询能被索引完全覆盖(即 SELECT 字段都在索引中),InnoDB 就无需回聚簇索引取数据——这意味着不仅快,而且锁得更少、更准。
- 举例:
SELECT user_id, amount FROM orders WHERE create_time > '2026-01-01',若存在联合索引INDEX idx_time_amt (create_time, user_id, amount),则只锁该索引上满足条件的记录,不涉及主键索引行 - 对比陷阱:若索引只有
(create_time),就要回表查user_id和amount,此时不仅性能差,还会额外锁定主键索引上的对应行,扩大锁竞争面 - 实操建议:对高频、带
FOR UPDATE或LOCK IN SHARE MODE的查询,优先设计覆盖索引;用SHOW ENGINE INNODB STATUS\G查看TRANSACTIONS部分的lock_mode和lock_trx_id,确认是否发生不必要回表
索引和锁的关系,本质是“数据库如何把逻辑意图翻译成物理锁定”。很多并发问题不是锁本身太重,而是索引没让锁落得准——一个没走索引的 UPDATE,可能比十个精心设计的行锁更伤并发。真正要盯住的,永远是那条 EXPLAIN 输出里的 key 和 rows。










