MySQL事务是如何通过undo、redolog实现的

107 阅读7分钟

MySQL事务是如何通过undo、redolog实现的

MySQL事务的实现是一个精密的过程,三种日志协同工作,确保ACID特性:

undo日志(保证原子性和隔离性)

  1. 工作原理

    1. 记录数据修改前的状态(旧值)

    2. 存储在系统表空间或单独的undo表空间中

    3. 形成版本链,支持MVCC机制

  2. 主要功能

    1. 事务回滚:当执行ROLLBACK或事务异常终止时,根据undo日志恢复数据

    2. MVCC 实现:不同事务可以根据隔离级别读取适当版本的数据,解决读写冲突

redo日志(保证持久性)

  1. 工作原理

    1. 采用WAL(预写式日志)机制

    2. 记录物理层面的页面修改操作

    3. 顺序写入,提高性能

  2. 主要功能

    1. 崩溃恢复:系统崩溃后,通过重放redo日志恢复未刷盘的已提交事务

    2. 性能优化:允许将随机写入转换为顺序写入,减少磁盘寻道开销

binlog(二进制日志)

  1. 工作原理

    1. 记录所有数据修改的逻辑操作

    2. 在事务提交时写入

    3. 支持多种格式(ROW、STATEMENT、MIXED)

  2. 主要功能

    1. 主从复制:向从库传输数据变更

    2. 时间点恢复:支持特定时间点的数据恢复

事务执行流程

  1. 事务开始:分配事务ID,开始记录变更

  2. 数据修改阶段

    1. 将原数据记录到undo日志

    2. 修改Buffer Pool中的数据页

    3. 将修改操作记录到redo日志缓冲区

    4. 根据配置可能会立即刷新redo日志到磁盘

  3. 事务提交阶段(两阶段提交):

    1. 准备阶段:确保redo日志已持久化

    2. 提交阶段

      • 写入binlog

      • 提交标记写入redo日志

      • 修改事务状态为已提交

  4. 后台操作

    1. 脏页异步刷盘

    2. 不再需要的undo日志逐渐被清理

这种机制确保了即使在系统崩溃的情况下,已提交的事务也不会丢失(持久性),未提交的事务能够回滚(原子性),同时保证了数据的一致性和隔离性。

MySQL事务通过undo、redo和binlog的深入实现机制

为了深入理解MySQL事务的内部实现,我将通过一个具体的银行转账例子,详细解析undo、redo和binlog日志的变化内容及其协同工作机制。

转账事务场景

假设有以下账户表和初始数据:

CREATE TABLE accounts (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    balance DECIMAL(10,2)
);

-- 初始数据
INSERT INTO accounts VALUES (1, 'UserA', 1000.00);
INSERT INTO accounts VALUES (2, 'UserB', 500.00);

现在执行一个转账事务:UserA向UserB转账100元

BEGIN;
UPDATE accounts SET balance = balance - 100.00 WHERE id = 1;
UPDATE accounts SET balance = balance + 100.00 WHERE id = 2;
COMMIT;

详细事务执行流程与日志变化

1. 事务开始 (BEGIN)

系统动作:

  • 分配事务ID (假设 trx_id = 100)

  • 为事务分配回滚段和undo日志空间

  • 创建读视图(Read View),包含当前活跃事务列表,用于MVCC

此时三种日志均未实际写入。

2. 第一条UPDATE执行

Buffer Pool变化:

  • UserA的余额在内存中被修改为900.00

  • 数据页被标记为"脏页"

undo 日志记录:

undo log entry:
trx_id: 100
table: accounts
operation: UPDATE
undo_ptr: 0x12345 (指向版本链)
columns: balance
old_values: 1000.00
row_pointer: 指向数据行的物理位置

这条undo记录是为了:

  1. 支持事务回滚

  2. 建立数据的历史版本链,支持MVCC并发控制

  3. 记录行的DB_TRX_ID和DB_ROLL_PTR系统列

redo日志缓冲区记录:

redo log entry:
LSN: 1000 (日志序列号)
trx_id: 100
operation: MODIFY
page_id: 100 (假设账户表在这个页)
offset: 150 (行在页中的偏移量)
field_offset: 12 (balance字段偏移)
before_value: 1000.00
after_value: 900.00

注意redo记录的是物理修改,而非SQL语句,包含足够的信息使系统能在崩溃后精确重建页面修改。

3. 第二条UPDATE执行

类似地,第二条UPDATE也会生成相应的undo和redo记录:

undo 日志记录:

undo log entry:
trx_id: 100
table: accounts
operation: UPDATE
undo_ptr: 0x12346
columns: balance
old_values: 500.00
row_pointer: 指向UserB行的位置

redo日志缓冲区记录:

redo log entry:
LSN: 1001
trx_id: 100
operation: MODIFY
page_id: 100
offset: 200
field_offset: 12
before_value: 500.00
after_value: 600.00

4. 提交阶段(两阶段提交协议)

当执行COMMIT时,MySQL启动两阶段提交过程,确保redo日志和binlog的一致性:

准备阶段(Prepare Phase)

redo日志操作:

  1. 将redo缓冲区内容刷新到磁盘
  2. 写入特殊的XA PREPARE记录:
redo log entry:
LSN: 1002
operation: XA PREPARE
trx_id: 100
  1. 调用fsync确保持久化

此时若系统崩溃,恢复时能识别处于prepare状态的事务。

提交阶段(Commit Phase)

binlog写入:

假设使用ROW格式binlog:

// 事务开始标记
binlog entry:
position: 1235
event_type: BEGIN
thread_id: 50

// 第一条UPDATE记录
binlog entry:
position: 1237
event_type: UPDATE_ROWS
table_id: 100
before_image: (1, 'UserA', 1000.00)
after_image: (1, 'UserA', 900.00)

// 第二条UPDATE记录
binlog entry:
position: 1239
event_type: UPDATE_ROWS
table_id: 100
before_image: (2, 'UserB', 500.00)
after_image: (2, 'UserB', 600.00)

// 事务结束标记
binlog entry:
position: 1240
event_type: XID
xid: 100

最终提交:

  1. binlog写入并持久化(由sync_binlog参数控制)
  2. redo日志写入最终COMMIT记录:
redo log entry:
LSN: 1003
operation: COMMIT
trx_id: 100
binlog_position: 1240
  1. 事务状态更新为"已提交"

  2. 释放行锁

5. 事务后台操作

脏页 刷盘:

  • Buffer Pool中的修改数据页不会立即写回磁盘

  • 由后台线程按某种策略(如最近最少使用)异步刷盘

  • 刷盘不影响事务提交的完成性

undo日志清理:

  • 事务提交后undo日志保留一段时间

  • 当没有活跃事务需要读取这些undo记录时

  • 后台Purge线程清理不再需要的undo日志

崩溃恢复机制分析

根据事务崩溃的不同时点,MySQL利用这三种日志进行一致性恢复:

场景1:准备阶段前崩溃

  • 恢复时发现redo日志中事务未标记为prepared或committed

  • 通过undo日志自动回滚所有更改

  • binlog中无记录,保持一致

场景2:准备阶段后,最终提交前崩溃

  • 这是关键场景,体现两阶段提交的重要性

  • 恢复时检查redo中prepared但未commit的事务

  • 再查询binlog,确定最终状态:

    • 若binlog中有完整记录,则提交(XA COMMIT)

    • 若binlog中无记录,则回滚(XA ROLLBACK)

场景3:最终提交后,脏页刷盘前崩溃

  • 恢复时通过redo日志重放已提交事务的所有修改

  • 将内存中未写入磁盘的更改应用到数据文件

  • 确保持久性

ACID特性与三种日志的对应关系

  • 原子性:主要依靠undo日志实现,确保事务要么全部成功,要么全部回滚

  • 一致性:由约束检查和完整事务过程保证,示例中转账前后总金额不变

  • 隔离性:通过undo日志的版本链和MVCC实现,不同事务互不干扰

  • 持久性:主要由redo日志保证,binlog提供额外的持久性保障

性能优化层面

  1. 组提交机制(Group Commit)

    1. 多个事务的redo和binlog可批量一起刷盘

    2. 大幅减少I/O操作,提高TPS

  2. writethrough与writeback

    1. redo日志采用writethrough方式(提交必须落盘)

    2. 数据文件采用writeback方式(延迟刷盘)

    3. 权衡性能与持久性

  3. binlog与redo日志同步策略

    1. sync_binlog=1: 每次事务提交都刷新binlog

    2. sync_binlog=0: 由操作系统决定flush时机,性能更好但有丢失风险

    3. innodb_flush_log_at_trx_commit控制redo日志写入策略

以上就是一个完整事务实现的深入分析,展示了MySQL如何巧妙地协调三种日志实现ACID特性,同时优化性能。