行锁依赖索引,无索引则退化为表锁;InnoDB通过record lock、gap lock和next-key lock实现行级锁定,其中next-key lock用于防止幻读但易引发死锁。

行锁是通过索引实现的,没索引就退化为表锁
MySQL 的 InnoDB 行锁(如 SELECT ... FOR UPDATE 或 UPDATE)本身不直接锁“行”,而是锁住索引记录(record lock)或索引间隙(gap lock)。这意味着:如果 WHERE 条件字段没有索引,InnoDB 无法快速定位目标行,只能走全表扫描,此时会为**扫描到的每条记录加锁**,实际效果等同于锁整张表。
常见错误现象:UPDATE users SET status=1 WHERE phone='138...' 执行极慢、并发更新卡死——查一下 EXPLAIN,发现 type 是 ALL,key 为 NULL,基本就是这个原因。
- 唯一索引等值查询 → 只锁匹配的那一条索引记录(record lock)
- 普通索引等值查询 → 锁匹配记录 + 对应的主键记录(因为二级索引不存完整行,需回表)
- 范围查询(如
WHERE id > 100)→ 可能触发 gap lock 或 next-key lock,锁住区间而非单行 - 无索引字段 → 全表扫描,逐行加锁,本质是表级争用
next-key lock 是行锁+间隙锁的组合,防幻读但易死锁
InnoDB 默认事务隔离级别是 REPEATABLE READ,为防止幻读,它用 next-key lock(前开后闭区间)代替单纯的 record lock。比如 SELECT * FROM t WHERE a = 5 FOR UPDATE,若 a 是普通索引且值 5 存在,则锁住的是 (3,5] 或 (5,7] 这样的区间,具体取决于索引结构。
这带来两个关键影响:
- 看似只改一行,实际可能锁住一大片相邻记录,阻塞其他事务插入或更新邻近值
- 多个事务按不同顺序访问索引区间时极易形成循环等待 → 死锁。例如事务 A 先锁 (1,3] 再锁 (5,7],事务 B 反过来先锁 (5,7] 再锁 (1,3]
- 使用
SELECT ... LOCK IN SHARE MODE同样受 next-key lock 影响,不只是FOR UPDATE
显式加锁语句的行为差异:FOR UPDATE vs LOCK IN SHARE MODE
两者都走索引才能生效为行锁,但语义和兼容性完全不同:
-
SELECT ... FOR UPDATE:加排他锁(X),其他事务无法对该记录加任何锁(S 或 X),也无法修改 -
SELECT ... LOCK IN SHARE MODE:加共享锁(S),允许其他事务加 S 锁(读),但阻塞 X 锁(写) - 如果事务中混合使用这两种语句,且涉及相同记录,可能因锁升级或等待顺序引发死锁
- 注意:即使只读事务,只要用了显式锁,也会参与锁竞争,不是“只读就安全”
如何验证当前 SQL 是否真的走了行锁
不能只看执行计划,得看锁的实际持有情况。最直接的方式是查 INFORMATION_SCHEMA.INNODB_TRX 和 INFORMATION_SCHEMA.INNODB_LOCK_WAITS:
SELECT trx_id, trx_state, trx_query FROM INFORMATION_SCHEMA.INNODB_TRX;
再结合:
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;
如果看到 blocking_trx_id 非空,说明有锁等待;再关联 INNODB_LOCKS(MySQL 5.7+ 已弃用,8.0+ 用 performance_schema.data_locks)可确认锁类型和对象。
容易被忽略的一点:事务未提交时,锁一直持有。哪怕你的应用层认为“SQL 执行完了”,只要没 COMMIT 或 ROLLBACK,锁就还在,可能长时间阻塞别人。


















