Skip to content

如果没有 select * from t where id = 1 for update 这条记录,会锁什么?能插入 id = 1 吗?能插入 id = 2 吗?

约 867 字大约 3 分钟

MySQL快手

2025-03-19

⭐ 题目日期:

快手 - 2024/12/29

📝 题解:

当执行 SELECT * FROM t WHERE id = 1 FOR UPDATE 且表中 没有 id = 1 的记录 时,加锁的行为取决于 索引类型事务隔离级别,进而影响能否插入 id = 1id = 2。以下是详细分析:


一、默认隔离级别(REPEATABLE READ)下的锁行为

假设事务隔离级别为 REPEATABLE READ(MySQL 默认),且 id 字段 有索引(主键、唯一索引或普通索引):

1. 加锁范围

  • InnoDB 会加 间隙锁(Gap Lock),锁定 id = 1 所在的 间隙范围,防止其他事务插入该间隙。
  • 示例: 假设表中现有数据为 id = 0id = 5,则 id = 1 位于间隙 (0, 5) 内。 执行 SELECT ... FOR UPDATE 后,会锁定间隙 (0, 5),阻止其他事务插入 id = 1id = 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 所在的间隙。
  • 示例: 若当前最大 id5,则锁定间隙 (5, +∞),阻止插入 id > 5 的值(如 6)。 注意:不同数据库版本可能有差异,需结合具体场景测试。

2. id 是普通索引或无索引

  • 普通索引:锁定 id = 1 所在的间隙。
  • 无索引:触发全表扫描,可能加 表级锁所有间隙锁,导致所有插入操作被阻塞。

三、不同隔离级别的行为差异

1. READ COMMITTED 隔离级别

  • 无间隙锁:若 id = 1 不存在,则 不会加任何锁
  • 插入行为
    • 插入 id = 1:✅ 允许。
    • 插入 id = 2:✅ 允许。
  • 风险:可能导致 幻读(同一事务中多次查询结果不一致)。

2. REPEATABLE READ 隔离级别

  • 有间隙锁:锁定 id = 1 所在的间隙,阻止插入。
  • 插入行为
    • 插入 id = 1id = 2(在间隙内):❌ 被阻塞。
    • 插入 id 不在间隙内:✅ 允许。

四、具体场景示例

场景 1:表中无任何数据

  • 执行 SELECT ... FOR UPDATE
    • id 是主键,会锁定区间 (-∞, +∞),阻止所有插入操作。
    • id 是普通索引或无索引,行为类似。

场景 2:表中有 id = 0 和 id = 5

  • 间隙范围(0, 5)
  • 插入 id = 1:被阻塞(位于间隙内)。
  • 插入 id = 6:允许(位于间隙外)。

五、如何验证锁行为?

  1. 查看锁信息

    SHOW ENGINE INNODB STATUS;          -- 查看当前锁和事务状态
    SELECT * FROM information_schema.INNODB_LOCKS;  -- 查看活跃锁
  2. 模拟并发操作

    • 在事务A中执行 SELECT ... FOR UPDATE
    • 在事务B中尝试插入,观察是否阻塞。

六、总结

  • 没有 id = 1 的记录时
    • REPEATABLE READ:加间隙锁,阻止插入锁定的间隙内的值(如 id = 1id = 2)。
    • READ COMMITTED:不加锁,允许插入任意值。
  • 关键点
    1. 索引和隔离级别 共同决定锁的范围。
    2. 无索引时可能升级为表级锁,严重影响并发性能。
    3. 实际开发中建议添加合适索引,并合理选择隔离级别。