外观
如果没有 select * from t where id = 1 for update 这条记录,会锁什么?能插入 id = 1 吗?能插入 id = 2 吗?
⭐ 题目日期:
快手 - 2024/12/29
📝 题解:
当执行 SELECT * FROM t WHERE id = 1 FOR UPDATE
且表中 没有 id = 1 的记录 时,加锁的行为取决于 索引类型 和 事务隔离级别,进而影响能否插入 id = 1
或 id = 2
。以下是详细分析:
一、默认隔离级别(REPEATABLE READ)下的锁行为
假设事务隔离级别为 REPEATABLE READ
(MySQL 默认),且 id
字段 有索引(主键、唯一索引或普通索引):
1. 加锁范围
- InnoDB 会加 间隙锁(Gap Lock),锁定
id = 1
所在的 间隙范围,防止其他事务插入该间隙。 - 示例: 假设表中现有数据为
id = 0
和id = 5
,则id = 1
位于间隙(0, 5)
内。 执行SELECT ... FOR UPDATE
后,会锁定间隙(0, 5)
,阻止其他事务插入id = 1
或id = 2
(因为2
也在该间隙内)。
2. 能否插入 id = 1 或 id = 2?
- 插入 id = 1:❌ 被阻塞,因为
id = 1
位于锁定的间隙内。 - 插入 id = 2:❌ 被阻塞(同理,
id = 2
也在间隙(0, 5)
内)。 - 插入 id = 6:✅ 允许(不在锁定的间隙内)。
二、不同索引类型的影响
1. id 是主键或唯一索引
- 加锁范围:即使
id = 1
不存在,仍会锁定id = 1
所在的间隙。 - 示例: 若当前最大
id
为5
,则锁定间隙(5, +∞)
,阻止插入id > 5
的值(如6
)。 注意:不同数据库版本可能有差异,需结合具体场景测试。
2. id 是普通索引或无索引
- 普通索引:锁定
id = 1
所在的间隙。 - 无索引:触发全表扫描,可能加 表级锁 或 所有间隙锁,导致所有插入操作被阻塞。
三、不同隔离级别的行为差异
1. READ COMMITTED 隔离级别
- 无间隙锁:若
id = 1
不存在,则 不会加任何锁。 - 插入行为:
- 插入
id = 1
:✅ 允许。 - 插入
id = 2
:✅ 允许。
- 插入
- 风险:可能导致 幻读(同一事务中多次查询结果不一致)。
2. REPEATABLE READ 隔离级别
- 有间隙锁:锁定
id = 1
所在的间隙,阻止插入。 - 插入行为:
- 插入
id = 1
或id = 2
(在间隙内):❌ 被阻塞。 - 插入
id
不在间隙内:✅ 允许。
- 插入
四、具体场景示例
场景 1:表中无任何数据
- 执行 SELECT ... FOR UPDATE:
- 若
id
是主键,会锁定区间(-∞, +∞)
,阻止所有插入操作。 - 若
id
是普通索引或无索引,行为类似。
- 若
场景 2:表中有 id = 0 和 id = 5
- 间隙范围:
(0, 5)
。 - 插入 id = 1:被阻塞(位于间隙内)。
- 插入 id = 6:允许(位于间隙外)。
五、如何验证锁行为?
查看锁信息:
SHOW ENGINE INNODB STATUS; -- 查看当前锁和事务状态 SELECT * FROM information_schema.INNODB_LOCKS; -- 查看活跃锁
模拟并发操作:
- 在事务A中执行
SELECT ... FOR UPDATE
。 - 在事务B中尝试插入,观察是否阻塞。
- 在事务A中执行
六、总结
- 没有 id = 1 的记录时:
- REPEATABLE READ:加间隙锁,阻止插入锁定的间隙内的值(如
id = 1
或id = 2
)。 - READ COMMITTED:不加锁,允许插入任意值。
- REPEATABLE READ:加间隙锁,阻止插入锁定的间隙内的值(如
- 关键点:
- 索引和隔离级别 共同决定锁的范围。
- 无索引时可能升级为表级锁,严重影响并发性能。
- 实际开发中建议添加合适索引,并合理选择隔离级别。