数据库并发控制核心知识笔记

2 天前
/ , , , ,

数据库并发控制核心知识笔记

一、 事务并发带来的问题

当多个事务同时访问和修改共享数据时,如果没有有效的控制机制,可能会引发以下几种数据不一致的问题:

  1. 脏读 (Dirty Read):
    • 定义: 一个事务读取到了另一个事务尚未提交的数据。
    • 风险: 如果另一个事务最终回滚(Rollback),那么读取到的数据就是无效的“脏”数据。
  2. 不可重复读 (Non-Repeatable Read):
    • 定义: 在同一个事务内,两次读取同一行数据,得到的结果却不一致。
    • 核心: 关注的是单行数据的 UPDATE 或 DELETE 操作。
    • 示例: 事务A第一次读取某行数据,事务B修改了该行并提交,事务A第二次读取时,发现数据内容已改变。
  3. 幻读 (Phantom Read):
    • 定义: 在同一个事务内,两次执行同一个范围查询,第二次查询的结果集包含了第一次查询中未出现的“幻影”行。
    • 核心: 关注的是某个数据范围的 INSERT 操作。
    • 示例: 事务A第一次查询某个范围的数据,事务B在该范围内插入了新数据并提交,事务A第二次查询时,发现多出了新的数据行。

二、 解决方案:事务隔离级别与MVCC

为了解决上述问题,SQL标准定义了四种事务隔离级别。MySQL的InnoDB存储引擎通过多版本并发控制(MVCC)锁机制的结合来实现这些级别。

  • MVCC (Multi-Version Concurrency Control): 是实现“读不加锁”的核心机制。它为每一行数据都维护了多个版本(通过隐藏列trx_id和roll_pointer)。当一个事务开始时,它会获得一个“快照”(Read View),在事务期间,普通的SELECT(快照读)只会看到在该快照创建之前就已经提交的数据版本。
    • 主要作用: 解决【不可重复读】问题。

三、 锁的核心机制

锁是保证数据一致性的强制手段,主要分为共享锁排他锁两大类。

  1. 共享锁 (Share Lock / S Lock):
    • 别称: 读锁。
    • 特性: 多个事务可以同时对同一数据持有共享锁。共享锁之间不互斥。
    • 作用: 允许并发读取,但阻止任何事务获取排他锁(即阻止写操作)。
    • 触发: SELECT ... LOCK IN SHARE MODE;
  2. 排他锁 (Exclusive Lock / X Lock):
    • 别称: 写锁。
    • 特性: 在任何时候,一份数据上只能有一个排他锁。排他锁与任何其他锁(包括共享锁)都互斥。
    • 作用: 在数据被修改时,阻止任何其他事务的读或写操作,保证数据修改的原子性。
    • 触发: UPDATE, DELETE, INSERT 会自动施加。也可手动触发:SELECT ... FOR UPDATE;

四、 InnoDB中的锁粒度与幻读的终极解决方案

InnoDB的锁不仅仅是锁住一行数据那么简单,它有更精细的粒度来解决幻读问题。

  1. 行锁 (Record Lock):
    • 锁定目标单个已存在的索引记录
    • 作用: 锁定一个“实体”,防止 UPDATE 和 DELETE。这是共享锁和排他锁的基本表现形式。
  2. 间隙锁 (Gap Lock):
    • 锁定目标索引记录之间的逻辑空位(开区间)
    • 作用: 锁定一片“虚空”,防止其他事务在此范围内 INSERT 新数据。这是解决【幻读】问题的关键。
    • 特性: 间隙锁之间是兼容的,即多个事务可以同时在同一个间隙上持有间隙锁。
  3. 临键锁 (Next-Key Lock):
    • 定义行锁 + 该行记录之前的间隙锁的组合。
    • 锁定目标: 一个已存在的索引记录及其前面的逻辑空位(左开右闭区间)。
    • 作用: 这是InnoDB在可重复读(Repeatable Read)隔离级别下,执行范围查询时的默认锁策略,它通过同时锁定实体和间隙,完美地解决了【幻读】问题。

五、 核心疑问解答总结

  1. 为什么INSERT会加排他锁?
    • 它锁的不是尚未存在的数据,而是即将在索引中占据的那个位置。这本质上是在该索引记录上施加了一个排他的行锁,以保证主键或唯一索引的唯一性,防止并发插入冲突。
  2. 数据库中为什么会存在“间隙”?
    • 数据库中的行在物理上并非连续存储。我们所见的顺序是由索引提供的逻辑顺序。“间隙”是存在于索引中的、逻辑上的空位。这种设计是为了极大地提高 INSERT 和 DELETE 操作的性能,避免大规模数据迁移。
  3. 为什么共享锁无法防止幻读?
    • 因为共享锁(及其底层的行锁)只能锁定已经存在的数据行。而幻读是由新插入的数据行引起的。用一把只能锁住“实体”的锁,无法阻止在“虚空”(间隙)中诞生新的实体。只有间隙锁才能完成这个任务。
  4. SELECT COUNT(*)会加锁吗?
    • 可重复读隔离级别下,普通的SELECT COUNT(*)执行的是快照读(MVCC)不加锁
    • 只有当明确使用LOCK IN SHARE MODE或FOR UPDATE时,才会升级为当前读,并通过临键锁来防止幻读。
  5. 为什么临键锁是 (previous_key, current_key] 的形式?
    • 本质: 临键锁是数据库在扫描索引时,对索引结构的基本锁定单元。索引本身是逻辑上连续的,临键锁的设计旨在将这个连续的空间,无缝、无重叠地进行划分。
    • 左开右闭的哲学:
      • 避免重叠: 如果是闭区间 [a, b],那么下一个锁 [b, c] 就会导致 b 被重复锁定。
      • 避免遗漏: 如果是开区间 (a, b),那么 a 和 b 这两个实体本身就会被遗漏。
      • 结论: 只有左开右闭的设计,才能像拉链一样,将整个索引空间 (负无穷, 正无穷) 完美地、不多不少地分割成一系列连续的锁定单元,确保了并发控制的严谨性。

六、 读取模式与锁的触发

数据库的读取操作分为两种模式,这直接决定了是否会触发复杂的锁机制:

  1. 快照读 (Consistent Read)
    • 触发条件: 普通的 SELECT 语句。
    • 机制: 基于MVCC,读取事务开始时创建的数据快照。
    • 行为不加任何锁。它通过读取历史版本的数据来避免与其他事务的写操作发生冲突。这是“可重复读”隔离级别能够解决“不可重复读”问题的根本原因。
  2. 当前读 (Current Read)
    • 触发条件: SELECT ... FOR UPDATE, SELECT ... LOCK IN SHARE MODE, 以及所有的 INSERT, UPDATE, DELETE 操作。
    • 机制: 读取数据库中最新提交的版本,并对读取到的记录施加锁
    • 行为:
      • 可重复读隔离级别下,当前读会使用临键锁 (Next-Key Lock)来锁定扫描到的索引范围,从而彻底解决幻读问题。
      • 普通的SELECT COUNT(*)属于快照读,因此不会产生幻读,也不会加锁。

使用社交账号登录

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...