MySQL存储引擎之：`InnoDB`的`Checkpoints`：其在`Redo Log`和`Buffer Pool`中的协同工作。

admin
Mon, 01 Sep 2025 10:13:26 +0000
Mysql, checkpoints, innodb, mysql

MySQL InnoDB 存储引擎：Checkpoints 在 Redo Log 和 Buffer Pool 中的协同工作 大家好，今天我们来深入探讨 InnoDB 存储引擎中的一个关键概念：Checkpoints。理解 Checkpoints 对于理解 InnoDB 的崩溃恢复机制、性能优化至关重要。我们将围绕 Checkpoints 在 Redo Log 和 Buffer Pool 中的协同工作展开，并通过示例代码和详细的逻辑分析，让大家彻底掌握这个知识点。 一、InnoDB 的数据持久化机制：Redo Log 和 Buffer Pool 在深入 Checkpoints 之前，我们先回顾一下 InnoDB 的数据持久化机制，这涉及 Redo Log 和 Buffer Pool 两个核心组件。 Buffer Pool: Buffer Pool 是 InnoDB 在内存中维护的一个缓存区域，用于存放经常访问的数据页。所有对数据的修改，首先在 Buffer Pool 中进行，然后再异步地刷回磁盘。 Redo Log: Redo Log 是一种基于磁盘的日志文件，用于记录对数据库的修改操作 …

继续阅读“MySQL存储引擎之：`InnoDB`的`Checkpoints`：其在`Redo Log`和`Buffer Pool`中的协同工作。”

The post MySQL存储引擎之：`InnoDB`的`Checkpoints`：其在`Redo Log`和`Buffer Pool`中的协同工作。 first appeared on 智猿学院-前后端，数据库，人工智能，云计算等领域前沿技术讲座.

MySQL InnoDB 存储引擎：Checkpoints 在 Redo Log 和 Buffer Pool 中的协同工作

大家好，今天我们来深入探讨 InnoDB 存储引擎中的一个关键概念：Checkpoints。理解 Checkpoints 对于理解 InnoDB 的崩溃恢复机制、性能优化至关重要。我们将围绕 Checkpoints 在 Redo Log 和 Buffer Pool 中的协同工作展开，并通过示例代码和详细的逻辑分析，让大家彻底掌握这个知识点。

一、InnoDB 的数据持久化机制：Redo Log 和 Buffer Pool

在深入 Checkpoints 之前，我们先回顾一下 InnoDB 的数据持久化机制，这涉及 Redo Log 和 Buffer Pool 两个核心组件。

Buffer Pool: Buffer Pool 是 InnoDB 在内存中维护的一个缓存区域，用于存放经常访问的数据页。所有对数据的修改，首先在 Buffer Pool 中进行，然后再异步地刷回磁盘。

Redo Log: Redo Log 是一种基于磁盘的日志文件，用于记录对数据库的修改操作。当 Buffer Pool 中的数据页被修改后，相应的修改操作会首先被写入 Redo Log，然后再异步地刷回磁盘。Redo Log 的主要作用是保证数据库的持久性和在崩溃后的数据恢复。

这种机制的核心思想是“Write-Ahead Logging (WAL)”，即先写日志，再写数据。这意味着，即使数据库服务器崩溃，Redo Log 中记录的修改操作也可以被用来恢复数据，从而保证数据的一致性。

二、Checkpoints 的概念和作用

Checkpoints 可以理解为 InnoDB 中一个关键的“快照”事件。它将 Buffer Pool 中被修改过的数据页（脏页）刷新到磁盘，并将对应的 Redo Log 中的记录标记为已应用。

Checkpoints 的主要作用如下：

1. 缩短数据库恢复时间： 在发生崩溃时，InnoDB 只需要从最近一次 Checkpoint 开始重做 Redo Log 中的记录，而不需要重做整个 Redo Log。

2. 回收 Redo Log 空间： 当 Checkpoint 发生后，在 Checkpoint 之前的 Redo Log 记录已经不再需要用于恢复，可以被覆盖重用。

3. 提高数据库性能： 通过定期将脏页刷新到磁盘，可以减少在查询时需要从磁盘读取数据的次数，从而提高查询性能。

三、Checkpoints 的类型

InnoDB 支持多种类型的 Checkpoints，主要包括：

1. Sharp Checkpoint: 停止所有活动，将所有脏页刷新到磁盘。这种方式会造成服务中断，仅在数据库关闭时使用。

2. Fuzzy Checkpoint: 允许数据库继续运行，并异步地将脏页刷新到磁盘。InnoDB 主要使用 Fuzzy Checkpoint。 Fuzzy Checkpoint 又分为以下几种类型：

Master Thread Checkpoint: 由 Master Thread 周期性地执行，用于控制整体的脏页刷新速度。

Page Cleaner Thread Checkpoint: 由 Page Cleaner 线程执行，用于并行地刷新脏页。

Async Flush Checkpoint: 当 Redo Log 的空间即将用完时触发，用于加速脏页的刷新。

LRU Flush Checkpoint: 当 Buffer Pool 中可用空间不足时触发，用于释放 Buffer Pool 中的页面。

四、Checkpoints 在 Redo Log 和 Buffer Pool 中的协同工作机制

现在我们来详细分析 Checkpoints 在 Redo Log 和 Buffer Pool 中的协同工作机制。

1. Redo Log 的写入：

当一个事务修改了 Buffer Pool 中的数据页时，InnoDB 会将相应的 Redo Log 记录写入 Redo Log Buffer。Redo Log 记录包含了修改的类型、修改的位置以及修改前后的数据。

// 假设一个简单的 Redo Log 记录结构
struct RedoLogRecord {
    int log_type;       // 日志类型，如 insert, update, delete
    int table_id;      // 表ID
    int page_number;    // 页号
    int offset;         // 偏移量
    char before_image[1024]; // 修改前的数据
    char after_image[1024];  // 修改后的数据
    long long lsn;        // Log Sequence Number
};

// 模拟 Redo Log 写入操作
void writeRedoLog(RedoLogRecord& record) {
    // ... 将 record 写入 Redo Log Buffer
    // 为 record 分配 LSN (Log Sequence Number)
    record.lsn = getNextLsn();
    // ... 将 Redo Log Buffer 中的内容刷新到 Redo Log 文件 (可能异步)
}

2. LSN (Log Sequence Number)：

LSN 是一个递增的序列号，用于标识 Redo Log 记录的位置。每个 Redo Log 记录都会被分配一个唯一的 LSN。InnoDB 使用 LSN 来跟踪 Redo Log 的写入进度和 Checkpoint 的位置。

3. Checkpoint 的触发：

Checkpoint 的触发条件有很多，例如：

达到预设的时间间隔。

Redo Log 的使用率达到一定的阈值。

脏页的数量达到一定的阈值。

4. Checkpoint 的执行：

当 Checkpoint 被触发时，InnoDB 会执行以下操作：

确定 Checkpoint 的位置（Checkpoint LSN）。Checkpoint LSN 是指所有在 Checkpoint 之前发生的 Redo Log 记录都已经应用到磁盘的 LSN。

将 Buffer Pool 中的脏页刷新到磁盘。InnoDB 会选择一部分脏页进行刷新，而不是全部。选择脏页的策略通常基于 LRU (Least Recently Used) 算法。

更新 InnoDB 的元数据，记录 Checkpoint LSN。

// 模拟 Checkpoint 操作
void performCheckpoint() {
    // 1. 确定 Checkpoint LSN
    long long checkpoint_lsn = getLatestLsn(); // 获取当前最大的 LSN

    // 2. 刷新脏页到磁盘
    flushDirtyPages(checkpoint_lsn);

    // 3. 更新元数据
    updateCheckpointMetadata(checkpoint_lsn);
}

void flushDirtyPages(long long checkpoint_lsn) {
    // 遍历 Buffer Pool，找到所有 dirty page
    for (auto& page : buffer_pool) {
        if (page.is_dirty && page.lsn <= checkpoint_lsn) {
            // 将 dirty page 写入磁盘
            writePageToDisk(page);
            page.is_dirty = false;
        }
    }
}

void updateCheckpointMetadata(long long checkpoint_lsn) {
    // 将 checkpoint_lsn 写入到磁盘上的元数据文件
    // ...
}

5. 崩溃恢复：

当数据库发生崩溃时，InnoDB 会使用 Redo Log 和 Checkpoint 信息进行恢复。

读取最近一次 Checkpoint 的 LSN。

从 Checkpoint LSN 开始，扫描 Redo Log，将 Redo Log 中记录的修改操作应用到磁盘上的数据页。

对于在崩溃时还没有刷新到磁盘的脏页，Redo Log 中的记录可以将其恢复到崩溃前的状态。

// 模拟崩溃恢复过程
void recoverFromCrash() {
    // 1. 读取最近一次 Checkpoint 的 LSN
    long long checkpoint_lsn = readCheckpointLsnFromMetadata();

    // 2. 从 Checkpoint LSN 开始，扫描 Redo Log
    scanRedoLog(checkpoint_lsn);
}

void scanRedoLog(long long checkpoint_lsn) {
    // 从 Redo Log 文件中读取 Redo Log 记录
    for (RedoLogRecord record : redo_log) {
        if (record.lsn > checkpoint_lsn) {
            // 将 Redo Log 记录应用到磁盘上的数据页
            applyRedoLogRecord(record);
        }
    }
}

void applyRedoLogRecord(RedoLogRecord& record) {
    // 根据 Redo Log 记录的类型，应用相应的修改操作
    // 例如，如果是 update 操作，则将数据页中的相应位置的数据更新为 after_image
    // ...
}

五、Checkpoints 的配置和监控

可以通过 MySQL 的配置参数来控制 Checkpoints 的行为。

参数名 | 说明
innodb_log_file_size | Redo Log 文件的大小。更大的 Redo Log 文件可以减少 Checkpoint 的频率，但会增加崩溃恢复的时间。
innodb_log_files_in_group | Redo Log 文件的数量。
innodb_max_dirty_pages_pct | Buffer Pool 中脏页的最大比例。当脏页的比例超过这个阈值时，InnoDB 会加速脏页的刷新。
innodb_io_capacity | InnoDB 的 I/O 能力。这个参数用于控制 InnoDB 刷新脏页的速度。
innodb_flush_method | 脏页刷新的方式。可以选择 O_DIRECT 或 fsync。O_DIRECT 绕过操作系统的缓存，直接将数据写入磁盘。fsync 使用操作系统的缓存。
innodb_lru_scan_depth | 用于控制 LRU 列表中要扫描的页数，以查找要刷新的脏页。增大这个值可以提高脏页的刷新速度，但也可能影响查询性能。
innodb_adaptive_flushing | 是否启用自适应刷新。启用自适应刷新后，InnoDB 会根据系统的负载自动调整脏页的刷新速度。
innodb_flush_neighbors | 控制在刷新脏页时，是否同时刷新相邻的脏页。
innodb_checkpoint_age | Redo Log 已用空间的量，超过此值会触发 Checkpoint。
innodb_log_checkpoint_nowait | 是否立即执行 Checkpoint，而不等待脏页刷新完成。通常不建议启用，因为它可能导致数据库性能下降。

可以通过 SHOW ENGINE INNODB STATUS 命令来监控 Checkpoints 的状态。例如，可以查看 Log sequence number 和 Last checkpoint at 来了解 Redo Log 的写入进度和 Checkpoint 的位置。

六、Checkpoints 的优化策略

以下是一些 Checkpoints 的优化策略：

1. 合理配置 Redo Log 文件的大小： Redo Log 文件过小会导致 Checkpoint 过于频繁，影响性能。Redo Log 文件过大会增加崩溃恢复的时间。需要根据实际 workload 选择合适的 Redo Log 文件大小。

2. 调整 innodb_max_dirty_pages_pct 参数： 适当降低 innodb_max_dirty_pages_pct 可以减少脏页的数量，降低 Checkpoint 的压力。

3. 使用高性能的存储设备： 使用 SSD 等高性能存储设备可以提高脏页的刷新速度，从而减少 Checkpoint 的延迟。

4. 监控 Checkpoints 的状态： 通过监控 Checkpoints 的状态，可以及时发现性能瓶颈并进行调整。

5. 避免长时间运行的大事务： 长时间运行的大事务会产生大量的 Redo Log 记录，导致 Checkpoint 压力增大。应该尽量将大事务拆分成小事务。

七、代码演示: 模拟 Redo Log 和 Checkpoint 的简单流程

以下是一个简化的 C++ 代码示例，用于模拟 Redo Log 和 Checkpoint 的基本流程。请注意，这只是一个演示，并不包含 InnoDB 存储引擎的全部复杂性。

#include <iostream>
#include <vector>
#include <fstream>

using namespace std;

// 简化的数据页结构
struct DataPage {
    int page_number;
    char data[1024];
    bool is_dirty;
    long long lsn; // 记录最后一次修改该页面的 LSN
};

// 简化的 Redo Log 记录结构
struct RedoLogRecord {
    int page_number;
    int offset;
    char before_image[100];
    char after_image[100];
    long long lsn;
};

long long current_lsn = 0; // 全局 LSN 计数器
vector<DataPage> buffer_pool;
vector<RedoLogRecord> redo_log;
string data_file = "data.db";
string redo_log_file = "redo.log";
string checkpoint_file = "checkpoint.info";

// 获取下一个 LSN
long long getNextLsn() {
    return ++current_lsn;
}

// 在 Buffer Pool 中查找数据页
DataPage* getPageFromBufferPool(int page_number) {
    for (auto& page : buffer_pool) {
        if (page.page_number == page_number) {
            return &page;
        }
    }
    return nullptr;
}

// 从磁盘读取数据页
DataPage readPageFromDisk(int page_number) {
    DataPage page;
    page.page_number = page_number;
    page.is_dirty = false;
    page.lsn = 0;

    ifstream file(data_file, ios::binary);
    if (file.is_open()) {
        file.seekg(page_number * sizeof(page.data));
        file.read(page.data, sizeof(page.data));
        file.close();
    } else {
        // 初始化数据
        memset(page.data, 0, sizeof(page.data));
    }
    return page;
}

// 将数据页写入磁盘
void writePageToDisk(DataPage& page) {
    ofstream file(data_file, ios::binary | ios::trunc | ios::out);
    if(file.is_open())
    {
        file.seekp(page.page_number * sizeof(page.data));
        file.write(page.data, sizeof(page.data));
        file.close();
    }
    else
    {
        cout << "failed to open data file!" << endl;
    }

}

// 写入 Redo Log
void writeRedoLog(RedoLogRecord& record) {
    record.lsn = getNextLsn();
    redo_log.push_back(record);

    // 持久化 Redo Log
    ofstream file(redo_log_file, ios::binary | ios::app);
    if (file.is_open()) {
        file.write(reinterpret_cast<char*>(&record), sizeof(record));
        file.close();
    }
}

// 修改数据
void modifyData(int page_number, int offset, const char* newData, int dataLength) {
    DataPage* page = getPageFromBufferPool(page_number);
    if (page == nullptr) {
        // 从磁盘读取数据页
        DataPage diskPage = readPageFromDisk(page_number);
        buffer_pool.push_back(diskPage);
        page = &buffer_pool.back();
    }

    // 记录修改前的状态
    RedoLogRecord record;
    record.page_number = page_number;
    record.offset = offset;
    memcpy(record.before_image, page->data + offset, dataLength);
    memcpy(record.after_image, newData, dataLength);

    // 应用修改
    memcpy(page->data + offset, newData, dataLength);
    page->is_dirty = true;
    page->lsn = getNextLsn(); // 记录修改的 LSN

    // 写入 Redo Log
    writeRedoLog(record);

    cout << "Modified page " << page_number << ", offset " << offset << ", lsn " << page->lsn << endl;
}

// 执行 Checkpoint
void performCheckpoint() {
    long long checkpoint_lsn = current_lsn;

    cout << "Performing checkpoint, LSN: " << checkpoint_lsn << endl;

    // 刷新脏页
    for (auto& page : buffer_pool) {
        if (page.is_dirty) {
            writePageToDisk(page);
            page.is_dirty = false;
            cout << "Flushed page " << page.page_number << " to disk." << endl;
        }
    }

    // 清空 Redo Log
    redo_log.clear();
    ofstream file(redo_log_file, ios::trunc); // 清空文件
    file.close();

    // 记录 Checkpoint 信息
    ofstream checkpointFile(checkpoint_file);
    if(checkpointFile.is_open())
    {
        checkpointFile << checkpoint_lsn;
        checkpointFile.close();
    }
    else
    {
        cout << "failed to open checkpoint file!" << endl;
    }

    cout << "Checkpoint complete." << endl;
}

// 恢复
void recover() {
    long long checkpoint_lsn = 0;

    // 读取 Checkpoint LSN
    ifstream checkpointFile(checkpoint_file);
    if (checkpointFile.is_open()) {
        checkpointFile >> checkpoint_lsn;
        checkpointFile.close();
        cout << "Recovering from checkpoint LSN: " << checkpoint_lsn << endl;
    } else {
        cout << "No checkpoint found. Starting recovery from beginning." << endl;
    }

    // 重做 Redo Log
    ifstream redoLogFile(redo_log_file, ios::binary);
    if (redoLogFile.is_open()) {
        RedoLogRecord record;
        while (redoLogFile.read(reinterpret_cast<char*>(&record), sizeof(record))) {
            if (record.lsn > checkpoint_lsn) {
                // 应用 Redo Log
                DataPage* page = getPageFromBufferPool(record.page_number);
                if (page == nullptr) {
                    // 从磁盘读取数据页
                    DataPage diskPage = readPageFromDisk(record.page_number);
                    buffer_pool.push_back(diskPage);
                    page = &buffer_pool.back();
                }
                memcpy(page->data + record.offset, record.after_image, sizeof(record.after_image));
                cout << "Applied redo log to page " << record.page_number << ", offset " << record.offset << ", lsn " << record.lsn << endl;
            }
        }
        redoLogFile.close();
    }
    else
    {
        cout << "no redo log to recover." << endl;
    }

    cout << "Recovery complete." << endl;
}

int main() {
    // 示例用法
    modifyData(1, 0, "Hello", 5);
    modifyData(1, 5, "World", 5);
    performCheckpoint();
    modifyData(2, 0, "New Data", 8);

    cout << "Simulating crash..." << endl;

    buffer_pool.clear(); // 模拟 Buffer Pool 内容丢失

    cout << "Starting recovery..." << endl;
    recover();

    return 0;
}

这个示例演示了以下流程：

1. 修改数据： modifyData 函数模拟了修改数据页的操作，并将修改记录写入 Redo Log。

2. Checkpoint： performCheckpoint 函数模拟了 Checkpoint 的过程，将脏页刷新到磁盘，并清空 Redo Log。

3. 恢复： recover 函数模拟了崩溃恢复的过程，从 Checkpoint LSN 开始重做 Redo Log。

编译并运行此代码，可以看到 Redo Log 和 Checkpoint 在数据持久化和恢复中的作用。

八、总结与思考

本次讲座我们深入探讨了 InnoDB 存储引擎中 Checkpoints 的概念、类型以及在 Redo Log 和 Buffer Pool 中的协同工作机制。 Checkpoints 通过将 Buffer Pool 中的脏页刷新到磁盘并标记 Redo Log 的位置，有效地缩短了崩溃恢复时间，并回收了 Redo Log 空间。 理解 Checkpoints 对于理解 InnoDB 的数据持久化和崩溃恢复机制至关重要，同时也为数据库性能优化提供了重要的思路。