本页内容受版权保护 · 已添加水印 · 禁止任何形式转载
事务与并发(ACID / 隔离级别 / 锁 / MVCC) · 锁机制

行锁是加在记录上还是索引上?没有索引时会发生什么?

Are row locks on records or on indexes? What happens without an index?

答案语言

考察要点

这道题面试官想看你是否深入理解 InnoDB 的锁实现细节,而不仅仅停留在“行锁”这个概念层面。核心考察点包括:

  1. InnoDB 的索引组织表(IOT)特性:理解数据行本身是存储在聚簇索引中的。
  2. 锁与索引的关系:明确锁是施加在索引记录上,而非数据页的物理行上。
  3. 无索引更新的后果:知道在没有合适索引时,行锁会退化(或称为“升级”)为事实上的表锁,及其对并发性能的毁灭性影响。

核心概念

面试官,关于这个问题,我的理解如下:

在 InnoDB 存储引擎中,行锁是施加在 索引记录 (Index Record) 上的,而不是我们通常想象中直接锁在数据行的物理存储位置上。

这个机制的根本原因在于 InnoDB 是一种索引组织表 (Index-Organized Table, IOT)。表中的数据行本身就存储在聚簇索引(通常是主键索引)的叶子节点上。因此,对行的任何操作,本质上都是对聚簇索引树的操作。

下面我将分“有索引”和“无索引”两种情况来详细解释:

1. 有索引的情况

当 UPDATE, DELETE, SELECT ... FOR UPDATE 等语句通过索引来定位记录时,InnoDB 会在对应的索引记录上加锁。

  • 使用主键索引 (Clustered Index) 如果 WHERE 条件命中了主键,InnoDB 会直接在聚簇索引中找到对应的叶子节点记录,并对其施加 Record Lock (记录锁)。

    mermaid
    1graph TD
    2 subgraph Clustered Index (B+ Tree)
    3 A[Root] --> B{Intermediate Node};
    4 B --> C1[Leaf: ID=10, Data='...'];
    5 B --> C2[Leaf: ID=20, Data='...'];
    6 B --> C3[Leaf: ID=30, Data='...'];
    7 end
    8
    9 style C2 fill:#f9f,stroke:#333,stroke-width:2px
    10
    11 subgraph SQL
    12 D[UPDATE users SET age = 31 WHERE id = 20;]
    13 end
    14
    15 D -- Locks --> C2;

    上图中,对 id = 20 的更新操作,直接锁住了聚簇索引中 ID=20 的那条索引记录。

  • 使用二级索引 (Secondary Index) 如果 WHERE 条件命中了二级索引,情况会复杂一些:

    1. 首先,会在二级索引中找到对应的索引记录,并对其加锁(通常是 Next-Key Lock)。
    2. 然后,通过二级索引中存储的主键值,回到聚簇索引中找到对应的数据行,并再次对聚簇索引的记录加锁。

    这个“回表”并再次加锁的过程是必要的,因为它能防止其他事务通过主键来并发修改同一行数据,从而保证数据一致性。

    sql
    1-- 假设 name 列上有二级索引
    2UPDATE users SET age = 31 WHERE name = 'Alice';

    这个操作会:

    1. 锁住 name 索引中 'Alice' 这一项。
    2. 锁住 name 索引中 'Alice' 对应的主键值(比如 id=20)在聚簇索引中的记录。

2. 无索引的情况

这是问题的关键,也是面试中的高频陷阱。当 UPDATE 或 DELETE 的 WHERE 条件列上没有可用的索引时,MySQL 无法通过索引定位到需要修改的记录,只能进行全表扫描。

在默认的 Repeatable Read (RR) 隔离级别下,为了防止幻读 (Phantom Reads),InnoDB 的锁定机制是:扫描过程中,对遇到的每一条聚簇索引记录,都会施加 Next-Key Lock。

Next-Key Lock = Record Lock + Gap Lock (间隙锁)

这意味着,InnoDB 不仅会锁住它扫描到的每一条存在的记录,还会锁住这些记录之间的间隙。当整个表都被扫描一遍后,其效果等同于锁住了整张表(虽然从技术上讲,它是由多个行锁和间隙锁构成的,但行为上和表锁无异),任何其他事务都无法对该表进行插入、更新、删除操作。

数据示例:

假设有 users 表:

id (PK)nameage
10Alice25
20Bob30
30Carol35

执行一个无索引的更新:

sql
1-- 假设 age 列上没有索引
2START TRANSACTION;
3UPDATE users SET name = 'Robert' WHERE age = 30;

在 RR 隔离级别下,MySQL 的执行过程:

  1. 开始全表扫描,从 id=10 开始。
  2. 检查 id=10 的行,age 不为 30。但为了防止幻读,依然会锁住 id=10 的记录以及它之前的间隙。
  3. 检查 id=20 的行,age 为 30,匹配成功。锁住 id=20 的记录。
  4. 继续扫描到 id=30,age 不为 30。锁住 id=30 的记录以及 id=20 和 id=30 之间的间隙。
  5. 扫描到表末尾,锁住 id=30 之后到无穷大的间隙。

最终结果是,整个 users 表的所有记录和所有间隙都被锁住,其他事务的任何写操作都会被阻塞,直到该事务提交或回滚。这就是所谓的锁升级 (Lock Escalation)。

常见低分回答

  1. “行锁就是锁在数据行上。”
    • 这个回答过于笼统,没有体现出对 InnoDB 索引组织表特性的理解。高分回答必须点出锁是作用于“索引记录”上。
  2. “没有索引就会用表锁。”
    • 虽然结果上类似,但这个说法不精确。InnoDB 本身没有纯粹的表级写锁(LOCK TABLES ... WRITE 是 Server 层的),其效果是通过给所有索引记录加锁来实现的。混淆了 MyISAM 的表锁和 InnoDB 的行锁升级行为。
  3. “只要更新一行,就算没索引也只会锁那一行。”
    • 这是最危险的错误。它完全忽略了 InnoDB 为了实现 RR 隔离级别而必须进行的扫描和锁定行为,这种误解在生产环境中会导致严重的并发性能问题和死锁。
  4. “没有索引会全表扫描,性能很差。”
    • 只说对了一半。除了性能差,更致命的是它对并发的影响。面试官更想听到的是“锁冲突”、“并发度下降”、“阻塞其他事务”等关键词。
  5. 忽略隔离级别。
    • 不提隔离级别就讨论锁行为是耍流氓。上述的 Next-Key Lock 和锁升级行为主要是在 RR 级别下。在 Read Committed (RC) 级别下,行为会有所不同(通常不使用 Gap Lock),虽然仍需全表扫描,但只会锁住匹配的行,并发性会好很多。

工程实践与优化

  • 核心原则:对于任何 UPDATE、DELETE 操作,其 WHERE 条件中涉及的列必须建立合适的索引。这是高性能并发数据库设计的铁律。
  • 执行计划分析:在上线任何写操作 SQL 前,务必使用 EXPLAIN 分析其执行计划。如果 UPDATE 或 DELETE 语句的 type 列显示为 ALL(全表扫描),并且 rows 列很大,这就是一个即将引爆系统的定时炸弹。
    sql
    1EXPLAIN UPDATE users SET name = 'Robert' WHERE age = 30;
    看到 type: ALL 就要立刻警觉,并为 age 列添加索引。
  • 问题定位:当生产环境出现大量线程处于 waiting for table metadata lock 或 Lock wait timeout exceeded 状态时,应首先怀疑是否存在无索引的更新/删除操作。
    • 使用 SHOW ENGINE INNODB STATUS; 查看 LATEST DETECTED DEADLOCK 和 TRANSACTIONS 部分,可以找到持有锁和等待锁的事务及其 SQL。
    • 在 MySQL 8.0+,使用 performance_schema.data_locks 和 performance_schema.data_lock_waits 表可以更精确地定位锁的持有者和等待者。
  • 特殊场景处理:如果确实需要执行一个无法使用索引的大批量数据订正任务,最佳实践是:
    1. 在业务低峰期执行。
    2. 将大事务拆分为多个小事务,按主键分批处理,每次处理一小部分数据(如 1000 行),处理完一批就提交事务,释放锁。
    3. 如果可以接受短时间的业务中断,可以先显式锁定全表 LOCK TABLES users WRITE;,执行完操作后 UNLOCK TABLES;,这样意图明确,避免了复杂的行锁争用。

常见追问与延伸题目

  1. 追问:如果我把隔离级别从 Repeatable Read (RR) 降到 Read Committed (RC),没有索引的更新会怎么样?

    • 回答要点:在 RC 隔离级别下,InnoDB 的锁策略会发生变化。Gap Lock (间隙锁) 会被禁用(除了外键约束检查和唯一性检查的特殊情况)。因此,即使 UPDATE 语句因没有索引而进行全表扫描,它也只会对扫描到并匹配成功的记录施加 Record Lock,而不会锁定不匹配的记录和它们之间的间隙。
    • 优缺点:
      • 优点:这极大地提高了并发性。其他事务仍然可以插入新记录,或者修改那些不满足 WHERE 条件的记录。
      • 缺点:它无法阻止幻读。在你执行 UPDATE 的事务中,如果再次查询,可能会发现有新的、符合 age=30 条件的行被其他事务插入了。这也是为什么很多互联网业务选择 RC 的原因:为了更高的并发性能,牺牲了一部分隔离性。
  2. 追问:SELECT ... FOR UPDATE 在没有索引的情况下,行为和 UPDATE 一样吗?

    • 回答要点:是的,行为完全一致。SELECT ... FOR UPDATE 是一个加锁读操作,其加锁逻辑与 UPDATE/DELETE 遵循相同的规则。如果 WHERE 条件没有索引,在 RR 级别下,它同样会进行全表扫描并对扫描过的所有记录施加 Next-Key Lock,从而锁住整张表。这是一个常见的性能陷阱,开发者可能以为只是想锁定几行数据,结果却导致整个系统挂起。
  3. 追问:如果表里一条数据都没有,UPDATE users SET age = 100 WHERE id = 1; 会加什么锁?

    • 回答要点:即使表是空的,InnoDB 仍然需要保证隔离性,防止其他事务插入 id=1 的记录。在 RR 级别下,由于 id 是主键,InnoDB 能够定位到这个不存在的记录应该在哪个间隙中。因此,它会施加一个 Gap Lock,锁住这个 id=1 应该插入的位置,防止其他事务向这个间隙中插入数据,直到当前事务结束。这同样是为了防止幻读。