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 的写入流程
当一个事务执行修改操作时,会经历以下步骤:
- 修改内存中的数据页(Buffer Pool): 首先,MySQL 会将数据页从磁盘加载到内存中的 Buffer Pool(缓冲池)。然后在 Buffer Pool 中修改数据。
- 生成 Redo Log: 针对 Buffer Pool 中的每一次修改,MySQL 会生成一条 Redo Log 记录,记录修改的位置和内容。
- 写入 Redo Log Buffer: Redo Log 记录首先会被写入 Redo Log Buffer(Redo Log 缓冲区)。
- 刷新 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
的表,包含 id
和 name
两列。
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 示例
假设我们有以下操作:
- 事务 T1 开始,修改了数据页 A。生成 Redo Log 记录 LSN 1000。
- 事务 T2 开始,修改了数据页 B。生成 Redo Log 记录 LSN 1010。
- 执行 Checkpoint,将数据页 A 和 B 刷新到磁盘,并记录 Checkpoint LSN 为 1010。
- 事务 T3 开始,修改了数据页 C。生成 Redo Log 记录 LSN 1020。
- 系统崩溃。
在崩溃恢复时,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 发生崩溃时,会按照以下流程进行恢复:
- 确定 Checkpoint LSN: MySQL 首先会读取 Checkpoint 信息,确定 Checkpoint LSN。
- 扫描 Redo Log: MySQL 从 Checkpoint LSN 开始扫描 Redo Log 文件,找到所有未提交的事务和已经提交但尚未完全刷入磁盘的事务。
- Redo(重做): 对于已经提交但尚未完全刷入磁盘的事务,MySQL 会根据 Redo Log 记录,将相应的修改重做(Redo)到数据页中。
- Undo(回滚): 对于未提交的事务,MySQL 会根据 Undo Log 记录(与 Redo Log 配合使用),将已经执行的修改回滚(Undo),以保证事务的原子性。
1. 崩溃恢复示例
继续沿用之前的例子,假设我们有以下操作:
- 事务 T1 开始,修改了数据页 A。生成 Redo Log 记录 LSN 1000。
- 事务 T2 开始,修改了数据页 B。生成 Redo Log 记录 LSN 1010。
- 执行 Checkpoint,记录 Checkpoint LSN 为 1010。
- 事务 T3 开始,修改了数据页 C。生成 Redo Log 记录 LSN 1020。
- 事务 T3 提交。
- 系统崩溃。
在崩溃恢复时,MySQL 会执行以下步骤:
- 确定 Checkpoint LSN 为 1010。
- 扫描 Redo Log,发现 LSN 1020 对应的事务 T3 已经提交,但数据页 C 尚未完全刷入磁盘。
- 根据 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 事务管理的关键。