Skip to content

事务的各个隔离级别是如何实现的

1. 锁机制 (Locking)

锁是实现隔离性的传统方式,其核心思想是当一个事务在访问某些数据时,阻止其他事务进行不兼容的操作。

  • 锁的类型

    • 共享锁 (Shared Lock, S锁):也叫读锁。多个事务可以同时持有同一份数据的共享锁,并读取该数据。但当数据上有S锁时,其他事务不能获取该数据的排他锁(X锁)。
    • 排他锁 (Exclusive Lock, X锁):也叫写锁。如果一个事务持有了某份数据的排他锁,那么其他任何事务都不能再获取该数据的任何锁(无论是S锁还是X锁),直到持有锁的事务释放。这保证了同一时间只有一个事务能修改数据。
  • 锁的粒度

    • 表级锁:锁定整张表,开销小,加锁快,但并发度最低。
    • 行级锁:锁定数据行,开销大,加锁慢,但并发度最高,能最大程度支持并发处理。InnoDB存储引擎支持行级锁。
    • 间隙锁 (Gap Lock):锁定一个范围,但不包括记录本身。主要用于防止幻读。
    • 临键锁 (Next-Key Lock):是记录锁和间隙锁的组合,锁定一个左开右闭的区间。这是InnoDB在可重复读隔离级别下解决幻读的关键。

2. 多版本并发控制 (Multi-Version Concurrency Control, MVCC)

MVCC是InnoDB引擎在读已提交可重复读隔离级别下处理读操作的核心机制。它的主要优势在于处理读-写冲突时无需加锁,实现了非阻塞的并发读取,极大地提升了数据库的并发性能。

MVCC的实现依赖于以下几个关键要素:

  • 隐藏字段:InnoDB会为每行数据添加三个隐藏字段。

    • DB_TRX_ID:记录最近一次创建或修改该行数据的事务ID。
    • DB_ROLL_PTR:回滚指针,指向该行记录的上一个版本,存储在Undo Log中。
    • DB_ROW_ID:隐藏的行ID,当表没有主键时,InnoDB会用它来生成聚簇索引。
  • Undo Log(回滚日志):Undo Log存储了数据的旧版本。当一个事务修改数据时,它会将修改前的数据版本存入Undo Log,形成一个版本链。DB_ROLL_PTR就是连接这个版本链的指针。

  • Read View(一致性视图):Read View是MVCC的核心,它决定了在当前事务中哪些数据版本是可见的。当事务启动时(或在特定隔离级别下,每条查询语句开始时),会创建一个Read View。 它主要包含以下内容:

    • m_ids:创建Read View时,当前系统中所有活跃(未提交)的事务ID列表。
    • min_trx_idm_ids列表中的最小事务ID。
    • max_trx_id:创建Read View时,系统应该分配给下一个事务的ID。
    • creator_trx_id:创建该Read View的事务的ID。
  • 可见性判断规则:当一个事务访问某行数据时,会用自己的Read View与该行记录的DB_TRX_ID进行比较,以判断该版本是否可见:

    1. 如果DB_TRX_ID等于creator_trx_id,说明是当前事务自己修改的,可见。
    2. 如果DB_TRX_ID小于min_trx_id,说明修改该数据的事务在当前事务启动前就已经提交,可见。
    3. 如果DB_TRX_ID大于或等于max_trx_id,说明修改该数据的事务在当前事务创建Read View之后才启动,不可见。
    4. 如果DB_TRX_IDmin_trx_idmax_trx_id之间,则需要检查m_ids列表。如果DB_TRX_IDm_ids中,说明修改该数据的事务在当前事务创建Read View时仍然活跃(未提交),因此不可见。如果不在m_ids中,说明已经提交,可见。

如果某个版本的数据对当前事务不可见,它就会通过DB_ROLL_PTR回滚指针去Undo Log中查找上一个版本,然后重复上述可见性判断,直到找到一个可见的版本为止。

四大隔离级别的实现

1. 读未提交 (Read Uncommitted)

  • 现象:允许脏读、不可重复读、幻读。
  • 实现思路和原理:这是最简单的隔离级别,几乎没有隔离性。在此级别下,读取数据不会加任何锁,也不使用MVCC。 它直接读取数据行最新的版本,即使这个版本是由一个尚未提交的事务所修改的。因此,性能最好,但数据一致性最差。

2. 读已提交 (Read Committed)

  • 现象:解决了脏读,但仍可能出现不可重复读、幻读。
  • 实现思路和原理:这是大多数数据库(如Oracle、PostgreSQL)的默认隔离级别。它的实现主要依赖MVCC。
    • 读操作 (SELECT)在事务中的每一条SELECT语句执行前,都会创建一个新的Read View。 这意味着,如果事务A在两次查询之间,事务B恰好修改了数据并提交,那么事务A的第二个SELECT会生成一个新的Read View,此时就能看到事务B提交的修改。这就导致了“不可重复读”。由于Read View只能看到已提交事务的修改,因此“脏读”问题被解决。
    • 写操作 (UPDATE, DELETE):写操作仍然需要加行级排他锁,以防止多个事务同时修改同一行数据。

3. 可重复读 (Repeatable Read)

  • 现象:解决了脏读、不可重复读,但理论上仍有幻读问题(InnoDB在很大程度上解决了)。 这是MySQL InnoDB引擎的默认隔离级别。
  • 实现思路和原理:此级别的实现结合了MVCC和锁机制。
    • 读操作 (快照读, 普通SELECT)只在事务开始后的第一次查询操作时创建一个Read View,并且整个事务期间都复用这一个Read View。 因为Read View是固定的,所以无论其他事务如何修改数据并提交,当前事务看到的始终是事务启动时的数据快照,从而保证了“可重复读”。
    • 写操作 (当前读, SELECT...FOR UPDATE, UPDATE, DELETE) 与幻读的解决
      • 标准的MVCC只能解决快照读的幻读问题。对于需要加锁的“当前读”操作,InnoDB通过Next-Key Lock(临键锁)来解决幻读。
      • 当一个事务执行当前读时,它不仅会锁定匹配到的记录(记录锁),还会锁定这些记录之间的“间隙”(间隙锁)。 这样一来,其他事务就无法在这个间隙中插入新的记录,从而防止了幻读的发生。

4. 串行化 (Serializable)

  • 现象:解决了脏读、不可重复读和幻读所有问题。
  • 实现思路和原理:这是最高级别的隔离,也是最影响并发性能的。
    • 实现机制:此级别下,InnoDB不再使用MVCC进行非锁定读。它会为所有的读操作(SELECT)隐式地加上共享锁(S锁),为写操作加上排他锁(X锁)。
    • 效果:当一个事务读取数据时,会加S锁,其他事务可以继续读取但不能修改。当一个事务写入数据时,会加X锁,其他事务既不能读也不能写。这种机制强制所有事务串行执行,一个接一个地处理,因此完全避免了并发问题,但并发性能也最低。
隔离级别 实现核心技术 读操作处理方式 写操作处理方式 解决的问题 存在的问题
读未提交 无锁 直接读取最新版本 加排他锁 (无) 脏读、不可重复读、幻读
读已提交 MVCC 每次SELECT创建新的Read View 加排他锁 脏读 不可重复读、幻读
可重复读 MVCC + Next-Key Lock 事务开始时创建唯一Read View 加排他锁和Next-Key Lock 脏读、不可重复读、幻读(大部分) (基本解决幻读)
串行化 读写锁 所有读操作加共享锁 所有写操作加排他锁 脏读、不可重复读、幻读 并发性能差