MySQL的redo log:在崩溃恢复时如何利用checkpoint机制保证事务的原子性与持久性?

MySQL Redo Log 与 Checkpoint:崩溃恢复中的事务保障

大家好,今天我们来深入探讨 MySQL 中 Redo Log 以及 Checkpoint 机制是如何共同协作,在崩溃恢复场景下保证事务的原子性(Atomicity)和持久性(Durability)的。这是理解 MySQL 事务可靠性的核心。

一、事务的 ACID 特性回顾

在深入 Redo Log 和 Checkpoint 之前,我们先快速回顾一下事务的 ACID 特性:

  • 原子性(Atomicity): 事务是不可分割的最小工作单元,要么全部成功,要么全部失败。
  • 一致性(Consistency): 事务执行前后,数据库必须保持一致性状态。
  • 隔离性(Isolation): 并发执行的事务之间相互隔离,互不干扰。
  • 持久性(Durability): 事务一旦提交,对数据库的修改就是永久性的,即使系统崩溃也不会丢失。

Redo Log 和 Checkpoint 机制主要负责保证事务的持久性,同时也间接影响原子性。

二、Redo Log:事务的重做日志

Redo Log,顾名思义,是“重做日志”。它记录了事务对数据页面的物理修改。也就是说,它记录了在哪个数据页面的哪个位置修改了哪些字节。

1. Redo Log 的写入流程

当一个事务执行修改操作时,会经历以下步骤:

  1. 修改内存中的数据页(Buffer Pool): 首先,MySQL 会将数据页从磁盘加载到内存中的 Buffer Pool(缓冲池)。然后在 Buffer Pool 中修改数据。
  2. 生成 Redo Log: 针对 Buffer Pool 中的每一次修改,MySQL 会生成一条 Redo Log 记录,记录修改的位置和内容。
  3. 写入 Redo Log Buffer: Redo Log 记录首先会被写入 Redo Log Buffer(Redo Log 缓冲区)。
  4. 刷新 Redo Log 到磁盘: Redo Log Buffer 会定期刷新到磁盘上的 Redo Log 文件中,以保证持久性。这个过程称为 log flush

2. Redo Log 的格式

Redo Log 的格式通常包含以下信息:

字段 描述
lsn Log Sequence Number,日志序列号,单调递增,用于标识 Redo Log 记录的顺序。
space_id 表空间 ID,标识 Redo Log 记录属于哪个表空间。
page_number 页号,标识 Redo Log 记录修改的是哪个数据页。
offset 偏移量,标识 Redo Log 记录修改的数据页中的具体位置。
data 修改的数据内容。
trx_id 事务 ID,标识 Redo Log 记录属于哪个事务。
prev_lsn 前一条 Redo Log 记录的 LSN,用于将同一个事务的 Redo Log 记录串联起来,形成 Redo Log 链。
next_undo_lsn 下一条 Undo Log 记录的 LSN,用于指向该事务的 Undo Log,以便在回滚时找到对应的 Undo Log。
type Redo Log 类型,例如:MLOG_REC_INSERT (插入记录), MLOG_REC_UPDATE (更新记录), MLOG_REC_DELETE (删除记录)等等。

3. Redo Log 的作用

Redo Log 的主要作用是在系统崩溃后,用于重做(Redo)已经提交但尚未完全刷入磁盘的数据页修改。 也就是说,Redo Log 确保了即使在 Buffer Pool 中的脏页(已修改但未刷入磁盘的数据页)丢失,也能通过 Redo Log 将数据恢复到崩溃前的状态,从而保证了事务的持久性。

4. Redo Log 的刷盘策略

MySQL 通过 innodb_flush_log_at_trx_commit 参数控制 Redo Log 的刷盘策略:

  • innodb_flush_log_at_trx_commit = 0 事务提交时,Redo Log 留在 Redo Log Buffer 中。master thread 每秒执行一次 flush 操作,将 Redo Log Buffer 中的内容刷到磁盘。可能丢失 1 秒内的事务数据。
  • innodb_flush_log_at_trx_commit = 1 (默认值): 事务提交时,Redo Log 直接刷到磁盘。性能较低,但安全性最高。
  • innodb_flush_log_at_trx_commit = 2 事务提交时,Redo Log 先写入操作系统的 page cache,然后由操作系统决定何时刷到磁盘。安全性介于 0 和 1 之间。

5. Redo Log 示例

假设我们有一个名为 users 的表,包含 idname 两列。

CREATE TABLE users (
    id INT PRIMARY KEY,
    name VARCHAR(255)
);

现在,我们执行一个事务:

START TRANSACTION;
INSERT INTO users (id, name) VALUES (1, 'Alice');
UPDATE users SET name = 'Bob' WHERE id = 1;
COMMIT;

在这个事务执行过程中,会生成如下的(简化)Redo Log 记录:

LSN space_id page_number offset data trx_id type
1000 1 123 0 (1, ‘Alice’) 1234 MLOG_REC_INSERT
1010 1 123 256 ‘Bob’ 1234 MLOG_REC_UPDATE

这些 Redo Log 记录会被写入 Redo Log Buffer,并最终刷到磁盘。

三、Checkpoint:数据页与 Redo Log 的协调者

Checkpoint 是一个关键机制,用于缩短崩溃恢复的时间,并减少需要重做的 Redo Log 数量。 Checkpoint 的本质是将 Buffer Pool 中的脏页(已修改但尚未刷入磁盘的数据页)刷新到磁盘,并记录下当前的 Checkpoint LSN。

1. Checkpoint 的类型

MySQL 中主要有两种类型的 Checkpoint:

  • Sharp Checkpoint: 停止所有新的事务,将所有脏页刷新到磁盘,然后记录 Checkpoint 信息。这种 Checkpoint 会造成数据库的短暂停顿,影响性能,因此不常用。
  • Fuzzy Checkpoint: 在后台异步地将脏页刷新到磁盘,不阻塞新的事务。这是 MySQL 常用的 Checkpoint 类型。

Fuzzy Checkpoint 又可以细分为多种类型,例如:

*   **Master Thread Checkpoint:** 由 Master Thread 定期触发的 Checkpoint。
*   **Age-Based Checkpoint:** 基于脏页的年龄(即上次修改的时间)触发的 Checkpoint。
*   **Percentage-Based Checkpoint:** 基于脏页在 Buffer Pool 中所占的比例触发的 Checkpoint。
*   **Log-Based Checkpoint:** 基于 Redo Log 的使用情况触发的 Checkpoint。

2. Checkpoint 的作用

  • 缩短恢复时间: Checkpoint 记录了数据库在某个时间点的数据状态。在崩溃恢复时,MySQL 只需要从 Checkpoint LSN 开始重做 Redo Log,而不需要从头开始,从而缩短了恢复时间。
  • 减少 Redo Log 的大小: 通过定期将脏页刷新到磁盘,可以释放 Redo Log 的空间,避免 Redo Log 文件过大。
  • 保证数据一致性: Checkpoint 保证了数据库在崩溃恢复后,能够恢复到一个一致的状态。

3. Checkpoint LSN

Checkpoint LSN 是指在执行 Checkpoint 时,已经写入磁盘的 Redo Log 的最大 LSN。 换句话说,Checkpoint LSN 之前的 Redo Log 记录对应的修改已经安全地写入了磁盘,不需要在崩溃恢复时重做。

4. Checkpoint 示例

假设我们有以下操作:

  1. 事务 T1 开始,修改了数据页 A。生成 Redo Log 记录 LSN 1000。
  2. 事务 T2 开始,修改了数据页 B。生成 Redo Log 记录 LSN 1010。
  3. 执行 Checkpoint,将数据页 A 和 B 刷新到磁盘,并记录 Checkpoint LSN 为 1010。
  4. 事务 T3 开始,修改了数据页 C。生成 Redo Log 记录 LSN 1020。
  5. 系统崩溃。

在崩溃恢复时,MySQL 会从 Checkpoint LSN (1010) 开始重做 Redo Log。 也就是说,只需要重做 LSN 1020 对应的修改,而不需要重做 LSN 1000 和 1010 对应的修改,因为数据页 A 和 B 已经安全地写入了磁盘。

5. 相关参数

innodb_log_checkpoint_interval:控制redo log checkpoint的触发频率,默认值是300秒。

innodb_io_capacity:用于控制后台IO的速率,会对checkpoint刷新脏页的速度产生影响。

四、崩溃恢复流程:Redo Log 和 Checkpoint 的协同工作

当 MySQL 发生崩溃时,会按照以下流程进行恢复:

  1. 确定 Checkpoint LSN: MySQL 首先会读取 Checkpoint 信息,确定 Checkpoint LSN。
  2. 扫描 Redo Log: MySQL 从 Checkpoint LSN 开始扫描 Redo Log 文件,找到所有未提交的事务和已经提交但尚未完全刷入磁盘的事务。
  3. Redo(重做): 对于已经提交但尚未完全刷入磁盘的事务,MySQL 会根据 Redo Log 记录,将相应的修改重做(Redo)到数据页中。
  4. Undo(回滚): 对于未提交的事务,MySQL 会根据 Undo Log 记录(与 Redo Log 配合使用),将已经执行的修改回滚(Undo),以保证事务的原子性。

1. 崩溃恢复示例

继续沿用之前的例子,假设我们有以下操作:

  1. 事务 T1 开始,修改了数据页 A。生成 Redo Log 记录 LSN 1000。
  2. 事务 T2 开始,修改了数据页 B。生成 Redo Log 记录 LSN 1010。
  3. 执行 Checkpoint,记录 Checkpoint LSN 为 1010。
  4. 事务 T3 开始,修改了数据页 C。生成 Redo Log 记录 LSN 1020。
  5. 事务 T3 提交。
  6. 系统崩溃。

在崩溃恢复时,MySQL 会执行以下步骤:

  1. 确定 Checkpoint LSN 为 1010。
  2. 扫描 Redo Log,发现 LSN 1020 对应的事务 T3 已经提交,但数据页 C 尚未完全刷入磁盘。
  3. 根据 LSN 1020 对应的 Redo Log 记录,将数据页 C 的修改重做(Redo)。

这样,即使在崩溃发生时,数据页 C 尚未完全刷入磁盘,MySQL 也能通过 Redo Log 将数据恢复到提交后的状态,从而保证了事务的持久性。

2. 代码模拟(简化)

以下是一个简化的 Python 代码,用于模拟崩溃恢复的过程:

class RedoLogEntry:
    def __init__(self, lsn, page_number, offset, data, trx_id):
        self.lsn = lsn
        self.page_number = page_number
        self.offset = offset
        self.data = data
        self.trx_id = trx_id

class Database:
    def __init__(self):
        self.pages = {} # 模拟数据页,key 是页号,value 是页内容

    def apply_redo_log(self, log_entry):
        page_number = log_entry.page_number
        offset = log_entry.offset
        data = log_entry.data

        if page_number not in self.pages:
            self.pages[page_number] = bytearray(4096)  # 假设页大小为 4096 字节

        page = self.pages[page_number]
        # 模拟写入数据
        for i in range(len(data)):
            page[offset + i] = data[i]

    def get_page_content(self, page_number):
        if page_number in self.pages:
            return self.pages[page_number]
        else:
            return None

# 模拟 Redo Log
redo_log = [
    RedoLogEntry(1000, 123, 0, b"Alice", 1),
    RedoLogEntry(1010, 123, 5, b"Bob", 2),
    RedoLogEntry(1020, 456, 0, b"Charlie", 3)
]

# 模拟 Checkpoint LSN
checkpoint_lsn = 1010

# 模拟数据库
db = Database()

# 模拟崩溃恢复
print("Starting crash recovery...")
for log_entry in redo_log:
    if log_entry.lsn > checkpoint_lsn:
        print(f"Applying redo log entry with LSN: {log_entry.lsn}")
        db.apply_redo_log(log_entry)

print("Crash recovery complete.")

# 验证恢复结果
page_123_content = db.get_page_content(123)
page_456_content = db.get_page_content(456)

if page_123_content:
    print(f"Page 123 content: {page_123_content[:10].decode()}") # 简化,只显示前10个字节
else:
    print("Page 123 not found.")

if page_456_content:
    print(f"Page 456 content: {page_456_content[:10].decode()}") # 简化,只显示前10个字节
else:
    print("Page 456 not found.")

注意: 这是一个高度简化的模型,省略了许多细节,例如 Undo Log、事务状态管理等等。 它的目的是为了帮助你理解 Redo Log 和 Checkpoint 在崩溃恢复中的基本作用。

五、Undo Log:事务的回滚日志

虽然今天主要聚焦 Redo Log 和 Checkpoint,但为了完整性,简单提一下 Undo Log。 Undo Log 记录了事务修改数据之前的状态(旧值)。 当事务需要回滚时,MySQL 会根据 Undo Log 记录,将数据恢复到原始状态。 Undo Log 和 Redo Log 共同保证了事务的原子性。

六、总结:Redo Log 和 Checkpoint 共同保障事务安全

Redo Log 记录对数据页的修改,确保即使在系统崩溃后也能重做已提交的事务。 Checkpoint 将脏页刷新到磁盘,缩短恢复时间,并减少需要重做的 Redo Log 数量。 两者协同工作,共同保证了 MySQL 数据库中事务的持久性和原子性,确保数据的一致性和可靠性。 理解 Redo Log 和 Checkpoint 机制是深入学习 MySQL 事务管理的关键。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注