行级锁不是一种独立锁,而是锁的粒度
很多人误以为“行级锁”和“共享锁/排他锁”是并列关系,其实不是:行级锁描述的是锁作用范围(一行数据),而共享锁(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,不是语句结束就释放
锁的复杂性不在概念多,而在它藏在索引结构、隔离级别、语句写法的交界处。调错一次,比写十次业务逻辑还耗神。
