行级锁是锁的粒度而非独立锁类型,本质是在索引记录上加共享锁(S锁)或排他锁(X锁);无索引时退化为表锁,锁行为受隔离级别、索引类型及语句写法共同影响。

行级锁不是一种独立锁,而是锁的粒度
很多人误以为“行级锁”和“共享锁/排他锁”是并列关系,其实不是:行级锁描述的是锁作用范围(一行数据),而共享锁(S锁)和排他锁(X锁)描述的是锁的行为语义(读 or 写)。InnoDB 的行级锁,本质上就是加在索引记录上的 S 锁或 X 锁。
也就是说:SELECT * FROM t WHERE id = 10 LOCK IN SHARE MODE 加的是「行级 + 共享锁」;UPDATE t SET name='a' WHERE id = 10 默认加的是「行级 + 排他锁」。
- 没索引?InnoDB 找不到精准的索引项可锁,就会退化为表级锁(哪怕你只改一行)
- 用
WHERE条件命中了二级索引?锁会加在该二级索引项上,且可能额外加主键索引上的锁(如当前读触发聚簇索引查找) - 事务隔离级别影响锁行为:比如
READ COMMITTED下,普通SELECT不加锁,但SELECT ... FOR UPDATE仍加行级 X 锁
共享锁与排他锁的兼容性决定并发表现
真正影响你程序是否卡住、是否死锁的,是 S 锁和 X 锁之间的兼容规则,而不是“是不是行级”。只要两个事务试图对同一行加不兼容的锁,后到的就会阻塞。
关键兼容表(仅针对同一行):
| S锁 | X锁 --------|-----|----- S锁 | ✅ | ❌ X锁 | ❌ | ❌
-
SELECT ... LOCK IN SHARE MODE→ 加 S 锁 → 其他事务还能加 S 锁(并发读),但不能加 X 锁(阻塞 UPDATE/DELETE) -
SELECT ... FOR UPDATE→ 加 X 锁 → 其他事务既不能加 S 锁也不能加 X 锁(完全互斥) - INSERT/UPDATE/DELETE 自动加 X 锁,无需显式写
FOR UPDATE,但要注意:如果WHERE条件没走索引,X 锁会升级为表锁
意向锁(IS/IX)是行锁和表锁共存的关键桥梁
当你执行 SELECT ... FOR UPDATE,InnoDB 实际做了两件事:先对整张表加 IX(意向排他锁),再对命中的行加 X 锁。这个 IX 是表级锁,但它本身不阻塞其他事务——它只起“声明作用”:告诉别人“我马上要对某些行加 X 锁了”。
- 没有
IX,当有人想对整张表加表级写锁(LOCK TABLES t WRITE),就得逐行检查有没有行锁,性能崩盘 -
IS和IX之间完全兼容,所以多个事务可以同时声明“我要读几行”或“我要改几行”,互不影响 - 但
IX与表级X锁互斥,IS与表级X锁也互斥——这才是表锁能快速判断“有人正在操作行”的原理
容易被忽略的坑:锁不是加在“数据行”上,而是加在“索引项”上
这是最常踩的坑。InnoDB 行锁永远绑定索引,哪怕你 SELECT * FROM t WHERE name = 'alice',如果 name 没有索引,InnoDB 就无法定位具体哪几行,只能全表扫描并给所有扫描过的行(甚至整个聚簇索引)加锁——结果等效于表锁。
- 查执行计划:
EXPLAIN SELECT ... FOR UPDATE,确认type是const/ref,不是ALL或index - 唯一索引和普通索引的锁范围不同:唯一索引等值查询只锁单条记录(Record Lock);非唯一索引等值查询会锁间隙(Gap Lock),防止幻读
- 显式开启事务后,哪怕只执行一条
SELECT ... FOR UPDATE,锁也会持续到COMMIT或ROLLBACK,不是语句结束就释放
锁的复杂性不在概念多,而在它藏在索引结构、隔离级别、语句写法的交界处。调错一次,比写十次业务逻辑还耗神。










