MySQL事务是如何通过undo、redolog实现的
MySQL事务的实现是一个精密的过程,三种日志协同工作,确保ACID特性:
undo日志(保证原子性和隔离性)
-
工作原理:
-
记录数据修改前的状态(旧值)
-
存储在系统表空间或单独的undo表空间中
-
形成版本链,支持MVCC机制
-
-
主要功能:
-
事务回滚:当执行ROLLBACK或事务异常终止时,根据undo日志恢复数据
-
MVCC 实现:不同事务可以根据隔离级别读取适当版本的数据,解决读写冲突
-
redo日志(保证持久性)
-
工作原理:
-
采用WAL(预写式日志)机制
-
记录物理层面的页面修改操作
-
顺序写入,提高性能
-
-
主要功能:
-
崩溃恢复:系统崩溃后,通过重放redo日志恢复未刷盘的已提交事务
-
性能优化:允许将随机写入转换为顺序写入,减少磁盘寻道开销
-
binlog(二进制日志)
-
工作原理:
-
记录所有数据修改的逻辑操作
-
在事务提交时写入
-
支持多种格式(ROW、STATEMENT、MIXED)
-
-
主要功能:
-
主从复制:向从库传输数据变更
-
时间点恢复:支持特定时间点的数据恢复
-
事务执行流程
-
事务开始:分配事务ID,开始记录变更
-
数据修改阶段:
-
将原数据记录到undo日志
-
修改Buffer Pool中的数据页
-
将修改操作记录到redo日志缓冲区
-
根据配置可能会立即刷新redo日志到磁盘
-
-
事务提交阶段(两阶段提交):
-
准备阶段:确保redo日志已持久化
-
提交阶段:
-
写入binlog
-
提交标记写入redo日志
-
修改事务状态为已提交
-
-
-
后台操作:
-
脏页异步刷盘
-
不再需要的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记录是为了:
-
支持事务回滚
-
建立数据的历史版本链,支持MVCC并发控制
-
记录行的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日志操作:
- 将redo缓冲区内容刷新到磁盘
- 写入特殊的XA PREPARE记录:
redo log entry:
LSN: 1002
operation: XA PREPARE
trx_id: 100
-
调用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
最终提交:
- binlog写入并持久化(由sync_binlog参数控制)
- redo日志写入最终COMMIT记录:
redo log entry:
LSN: 1003
operation: COMMIT
trx_id: 100
binlog_position: 1240
-
事务状态更新为"已提交"
-
释放行锁
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提供额外的持久性保障
性能优化层面
-
组提交机制(Group Commit) :
-
多个事务的redo和binlog可批量一起刷盘
-
大幅减少I/O操作,提高TPS
-
-
writethrough与writeback:
-
redo日志采用writethrough方式(提交必须落盘)
-
数据文件采用writeback方式(延迟刷盘)
-
权衡性能与持久性
-
-
binlog与redo日志同步策略:
-
sync_binlog=1: 每次事务提交都刷新binlog
-
sync_binlog=0: 由操作系统决定flush时机,性能更好但有丢失风险
-
innodb_flush_log_at_trx_commit控制redo日志写入策略
-
以上就是一个完整事务实现的深入分析,展示了MySQL如何巧妙地协调三种日志实现ACID特性,同时优化性能。