Featured image of post Java工程师 事务原理 & MVCC

Java工程师 事务原理 & MVCC

🌏Java工程师 事务原理 & MVCC 🎯 这篇文章用于记录事务原理 & MVCC的学习

事务的概念和特性

对于事务的四个特性 其中原子性、一致性、持久性依赖redo log和undo log实现,隔离性依赖锁机制和MVCC来实现

redo log 概述

redo log保证ACID中的D,即持久性。

为什么redo log也需要持久化到磁盘? 因为redo log的使命是在任何情况下都能保证已提交的事务(其日志在提交时已持久化)的数据绝不会丢失,为了应对极端情况,如系统断电(数据库崩溃)时或者数据刷盘时遇到故障,可以通过重放redo log日志实现数据的恢复。所以redo log必须先(Write-AHead Logging)持久化。

另一方面,系统通过redo log的快速持久化(顺序IO)来保证事务的快速响应(在用户执行 COMMIT 的瞬间,立即、可靠地确认这个事务已经成功)。而不是每次等事务真正的数据修改持久化(随机IO操作缓慢,耗时较长)后才响应。

undo log 概述

实现事务的原子性(Atomicity)和回滚(Rollback)

事务是一个不可分割的工作单位,事务中的操作要么全部成功,要么全部失败。

如何实现回滚(原子性)?

当一个事务对数据进行修改时(INSERT, UPDATE, DELETE),InnoDB 不仅会产生 Redo Log,还会产生 Undo Log

  1. 对于 UPDATEDELETE 操作
    • 生成内容:Undo Log 会记录修改前数据的旧版本(拷贝旧记录到 Undo Log)。
    • 目的:如果需要回滚,就可以根据这个记录,将数据恢复到修改前的状态。它逻辑上是执行一个逆向的 UPDATE(将新值改回旧值)或 INSERT(将删除的行再插回去)。
    • **什么情况下可删除?**不再被MVCC数据版本引用的时候。
  2. 对于 INSERT 操作
    • 生成内容:Undo Log 会记录新插入行的 主键信息
    • 目的:如果需要回滚,直接根据这个主键信息执行一个 DELETE 操作即可。
    • **什么情况下可删除?**事务提交后。

回滚过程: 当执行 ROLLBACK 时,InnoDB 会从 Undo Log 中读取相应的记录,并执行逆向操作来撤销本事务所做的修改。撤销完成后,该事务生成的 Undo Log 也就完成了使命。

实现多版本并发控制(MVCC - Multi-Version Concurrency Control)

MVCC:这是 MySQL 实现高并发的重要机制。它通过保存数据的多个历史版本,使得读操作(SELECT)不会阻塞写操作(UPDATE, DELETE),写操作也不会阻塞读操作,极大地提升了数据库的并发性能。

如何支实现MVCC(实现一致性读)?

这是 Undo Log 更精妙的用法。我们通过一个经典的“读-写”并发场景来看:

  1. 假设事务 A(事务ID=100)开始后,修改了行 R,将 name"Alice" 改为 "Bob"。这个修改过程同时写入了 Redo Log 和 Undo Log。
  2. 此时,事务 B(事务ID=101)开始,它希望读取行 R
  3. 为了保证事务 B 能看到一个一致的快照(Read View),InnoDB 不会直接让它读取当前最新的值("Bob"),因为事务 A 可能还没提交,这个数据是“脏”的。
  4. InnoDB 会通过行记录上的一个隐藏字段(DB_ROLL_PTR,回滚指针)顺藤摸瓜。这个指针指向了写入 Undo Log 中的上一个历史版本。
  5. 事务 B 沿着这个指针链,找到了事务 A 修改之前的版本(name = "Alice"),并将这个旧版本数据返回给用户。
  6. 这样,事务 B 实现了一次非阻塞的快照读(一致性读),而事务 A 的写操作也没有被阻塞。

undo log实现MVCC的复杂场景

场景 setup

我们有一张简单的表 accounts

id name balance DB_TRX_ID DB_ROLL_PTR
1 张三 1000.00 80 0x7a11c0
  • DB_TRX_ID (隐藏字段):最近一次修改该行数据的事务ID。
  • DB_ROLL_PTR (隐藏字段):指向该行上一个历史版本在 Undo Log 中的地址指针。

初始事务状态:

  • 事务 TRX_80 已提交,它创建了这条初始数据。

复杂操作序列

当前读和快照读

复杂操作序列举例

现在,三个事务按以下顺序开始操作,它们的开始时机和隔离级别决定了它们能看到什么。

时间点 T1:

  • TRX_90 开始 (这是一个写事务,假设隔离级别为 READ COMMITTEDREPEATABLE READ)
  • TRX_90 执行 UPDATE accounts SET balance = balance - 100 WHERE id = 1; (将余额减去100)

此时,InnoDB 的操作:

  1. 它不是直接覆盖原数据,而是先将当前行的数据拷贝到 Undo Log 中。Undo Log 中现在有一条记录:[Undo Record: id=1, name='张三', balance=1000.00, modified_by_TRX=80]
  2. 然后它才更新内存中 accounts 表的数据行:
    • balance = 900.00
    • DB_TRX_ID = 90 (更新为当前事务ID)
    • DB_ROLL_PTR 指向刚刚创建的 Undo Log 记录地址,比如 0x8b22d1
  3. 同时,这个修改也会记录到 Redo Log 以保证持久性。

此时,数据库中有两个版本的数据:

  • 当前版本(id=1, balance=900, TRX_ID=90, ROLL_PTR -> 0x8b22d1)
  • 历史版本(id=1, balance=1000, TRX_ID=80) (存储在 Undo Log 中,由 0x8b22d1 指向)

时间点 T2:

  • TRX_91 开始 (这是一个只读事务,隔离级别为 REPEATABLE READ)
  • TRX_91 执行 SELECT balance FROM accounts WHERE id = 1; 它要读取数据了!

MVCC 魔法开始:

  1. InnoDB 为 TRX_91 创建一个 Read View(一致性视图)。这个视图决定了 TRX_91 能看到哪些事务的修改。
    • 关键规则:它只能看到所有事务ID <= 91` 且已经提交的事务所做的修改。
    • 在 T2 时刻,TRX_90 (ID=90) 还未提交
  2. InnoDB 从最新的数据行开始检查:
    • 最新数据的 DB_TRX_ID = 90
    • TRX_91 的 Read View 检查:90 < 91,但事务90未提交 -> 此版本对当前事务不可见
  3. InnoDB 顺着回滚指针 ROLL_PTR (0x8b22d1) 找到 Undo Log 中的历史版本。
  4. 检查历史版本:DB_TRX_ID = 80
    • TRX_91 的 Read View 检查:80 < 91,且事务80已提交 -> 此版本对当前事务可见
  5. 因此,TRX_91 读取到的 balance1000.00,而不是最新的 900.00。它完美地避免了对未提交数据的脏读。

时间点 T3:

  • TRX_90 执行 COMMIT; 提交事务。

时间点 T4:

  • TRX_92 开始 (另一个只读事务,隔离级别为 READ COMMITTED)
  • TRX_92 执行 SELECT balance FROM accounts WHERE id = 1;

MVCC 再次工作 (不同隔离级别的差异):

  1. InnoDB 为 TRX_92 创建一个新的 Read View
    • 对于 READ COMMITTED,它的规则是:只看到在本语句执行前已经提交的事务。
    • 在 T4 时刻,TRX_90 (ID=90) 已经提交
  2. InnoDB 找到最新数据行:DB_TRX_ID = 90
  3. TRX_92 的 Read View 检查:90 < 92,且事务90已提交 -> 此版本对当前事务可见
  4. 因此,TRX_92 读取到的 balance900.00。它读到了已提交的最新数据。

时间点 T5:

  • TRX_91 再次执行 SELECT balance FROM accounts WHERE id = 1; (同一个事务内的第二次查询)

MVCC 的核心魅力 (可重复读):

  1. TRX_91 在 T2 时刻第一次查询时,就已经生成了它的 Read View
  2. 对于 REPEATABLE READ 隔离级别,一个事务只在第一次执行查询时创建 Read View,后续所有查询都复用这个视图
  3. 因此,即使在 T5 时刻 TRX_90 已经提交,TRX_91 的 Read View 规则依然不变:它依然看不到 TRX_90 的修改(因为在它创建视图时,TRX_90 未提交)。
  4. InnoDB 再次沿着版本链查找,找到的依然是 Undo Log 中那个由 TRX_80 创建的、已提交的版本。
  5. 因此,TRX_91 第二次查询读取到的 balance 依然是 1000.00。这就实现了“可重复读”,即同一事务内多次读取同一数据,结果是一致的。

undo log 总结

Undo Log 在 MVCC 中的复杂运用体现在:

  1. 构建版本链:每次修改都记录旧数据到 Undo Log,并通过回滚指针串联起来。
  2. 提供历史快照:当某个事务需要读取时,InnoDB 遍历这个版本链,并根据该事务的 Read View 的可见性规则,为它找到一个合适的、可见的历史版本
  3. 实现不同隔离级别
    • READ COMMITTED:每次查询都生成新 Read View,所以能看到最新已提交的版本。
    • REPEATABLE READ:第一次查询生成 Read View 后不再改变,所以总是看到同一个历史版本,实现可重复读。
  4. 清理机制:这些 Undo Log 版本不会永远存在。当没有任何现存的事务需要看到某个历史版本时(即没有比它更老的事务活跃时),这个版本的 Undo Log 就可以被 Purge 线程安全地清理掉。