MySQL事务原理与底层实现
版本说明:本文基于MySQL 8.x版本,内容涵盖InnoDB存储引擎的事务实现机制。
一、事务的基本概念与重要性
在Java后端开发中,我们习惯通过@Transactional注解或PlatformTransactionManager编程式开启事务,但真正理解事务,必须向下穿透到数据库内核层面。事务并非某个单一技术组件,而是一套协同工作的机制体系,它代表一个逻辑工作单元,由一系列相关的数据库操作组成。这些操作作为一个不可分割的整体,要么全部成功执行,要么全部失败回滚。事务机制是保证数据一致性、完整性和可靠性的关键技术基础。
经典事务示例:银行转账操作
-- 转账事务:从账户A向账户B转账100元
START TRANSACTION;
-- 检查账户A余额是否充足
SELECT balance INTO @current_balance FROM accounts WHERE account_id = 1;
IF @current_balance >= 100 THEN
-- 扣减账户A余额
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
-- 增加账户B余额
UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;
-- 记录交易日志
INSERT INTO transaction_log(from_account, to_account, amount, timestamp)
VALUES (1, 2, 100, NOW());
COMMIT; -- 所有操作成功,提交事务
ELSE
ROLLBACK; -- 余额不足,回滚事务
END IF;
二、ACID原则的底层实现机制
核心要点:
- 原子性:由
undo log保障,支持回滚与MVCC; - 持久性:由
redo log + WAL + checkpoint保障,双1配置是生产底线; - 隔离性:
MVCC实现高效读,锁保障写安全;RR非完全串行化,需警惕幻读; - 一致性:是ACID的终点,需应用层(
@Transactional)与数据库层协同设计。
2.1 原子性(Atomicity)
原子性确保事务中的操作要么全部成功,要么全部失败,其核心实现依赖于 undo log(回滚日志)。
InnoDB在执行每条DML语句前,会先将该操作的逆向信息写入undo log,再修改内存中的数据页。当事务提交时,undo log不会立即删除,而是标记为“可清理”;当事务回滚时,系统则依据undo log中的记录逆序执行恢复动作。
undo log的详细工作机制:
说明:该流程图揭示了原子性的实现根基——无论提交或回滚,系统均有明确路径:提交时仅标记undo待清理;失败时则逆序回放undo log,确保数据回归初始状态。
以一条UPDATE语句为例,其undo log记录的结构如下:
-- 以UPDATE操作为例的undo log记录
-- 原始数据: id=1, name='张三', balance=5000
-- 执行UPDATE语句:
UPDATE accounts SET balance = 4500 WHERE id = 1;
-- 生成的undo log包含:
-- 1. 事务ID: trx_id = 1001
-- 2. 回滚指针: roll_ptr = 0x123xxx
-- 3. 旧值记录:
-- 表空间: tablespace_id = 5
-- 记录位置: page_no = 123, offset = 456
-- 旧数据: balance = 5000
-- 4. 回滚SQL:
-- UPDATE accounts SET balance = 5000
-- WHERE id = 1 AND trx_id = 1001;
注意:undo log中的"回滚SQL"并非真实存储SQL文本,而是以物理+逻辑混合形式记录页偏移、字段偏移与旧值,确保回滚高效可靠。
生产实践:实际undo log以二进制格式存储在undo tablespace中(MySQL 8.0+ 默认独立表空间),支持purge线程异步清理。生产环境中,监控
innodb_undo_tablespaces参数,避免undo log膨胀导致空间不足。可通过SHOW ENGINE INNODB STATUS查看undo log使用情况。
2.2 持久性(Durability)
持久性确保一旦事务提交,其对数据的修改就是永久性的,即使系统崩溃也不会丢失。InnoDB通过 redo log(重做日志) 与 WAL(Write-Ahead Logging) 机制实现此目标。
WAL原则要求:任何数据页修改必须先写日志,再改内存页。redo log采用循环写入的物理日志格式,记录的是“将某页某偏移处的N字节改为XXX”,而非SQL语义,因此重放速度极快。
redo log的完整工作流程:
sequenceDiagram
participant Client as 客户端
participant Server as MySQL Server
participant LogBuffer as redo log buffer<br/>(内存)
participant LogFile as redo log file<br/>(磁盘)
participant BufferPool as Buffer Pool<br/>(内存)
participant DataFile as 数据文件<br/>(磁盘)
participant Checkpointer as 后台线程<br/>(Checkpointer)
Client->>Server: 发起事务修改请求(DML)
activate Server
Server->>LogBuffer: 写入 redo log(mini-transaction)
Note right of LogBuffer: 日志先入内存 buffer
Server->>BufferPool: 应用修改(脏页生成)
Note right of BufferPool: 数据页变“脏”,未落盘
Server->>Client: 事务提交成功(2PC 后)
deactivate Server
Note over LogBuffer,LogFile: 触发刷盘条件:<br/>• log buffer ≥ innodb_log_buffer_size/2<br/>• 事务提交(sync_binlog=1等)<br/>• 后台每秒刷一次
LogBuffer->>LogFile: 后台线程刷 redo 日志到磁盘
Note right of LogFile: fsync() 保证持久性
Note over BufferPool,DataFile: Checkpoint 机制:<br/>• LSN 推进<br/>• 脏页比例过高<br/>• 脏页存活过久
Checkpointer->>BufferPool: 扫描脏页
Checkpointer->>DataFile: 异步刷脏页到数据文件
Note right of DataFile: 可能早于/晚于 redo commit,<br/>崩溃恢复靠 redo 重做
DataFile-->>Client: 数据最终持久化完成(异步)
关键配置参数详解:
-- 查看redo log相关配置
SHOW VARIABLES LIKE 'innodb_log%';
-- 重要参数说明:
-- innodb_log_file_size: 每个redo log文件大小,建议设置1-2GB
-- innodb_log_files_in_group: redo log文件数量,通常为2-4个
-- innodb_log_buffer_size: redo log缓冲区大小,建议16-64MB
-- 持久性保证配置
SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit';
-- 参数值说明:
-- 1: 最高安全级别,每次提交都刷盘,保证零数据丢失(金融系统标准配置)
-- 2: 折中方案,日志写入OS缓存,每秒刷盘一次(大多数OLTP系统的推荐选择)
-- 0: 最高性能,每秒由后台线程刷盘一次(仅适用于可接受数据丢失的场景)
-- 生产建议:对于高并发系统,结合group commit机制(innodb_log_write_ahead_size)优化redo log写入
-- 通过SHOW ENGINE INNODB STATUS监控redo log等待时间
-- MySQL 8.0+支持redo log加密,可通过innodb_redo_log_encrypt启用
checkpoint机制详解: Checkpoint是InnoDB的重要机制,用于定期将内存中的脏页刷新到磁盘,并推进redo log的清理点。
- 模糊检查点(Fuzzy Checkpoint):渐进式刷盘,只确保某个LSN之前的修改已持久化,是正常运行时的默认方式
- Sharp检查点:确保所有脏页都刷盘,用于关闭实例、切换redo log文件等场景,会阻塞用户操作
- 检查点算法:基于LRU列表和Flush列表进行智能刷盘,由Page Cleaner线程执行
- 监控指标:通过
SHOW ENGINE INNODB STATUS中的Log sequence number和Last checkpoint at差值评估checkpoint压力
2.3 隔离性(Isolation)
隔离性通过锁机制和多版本并发控制(MVCC) 共同实现,确保并发事务之间相互隔离。
锁机制详细分类:
说明:InnoDB主要依赖行级锁实现高并发,页面锁在InnoDB中罕见。AUTO-INC锁用于自增列,MDL(元数据锁)在DDL操作中常见,是DDL与DML冲突的常见原因。
其中,行级锁是InnoDB高并发能力的核心。其加锁行为高度依赖查询条件与索引设计。例如,对主键等值查询:
-- 基于主键的等值查询,加记录锁
SELECT * FROM users WHERE id = 1 FOR UPDATE;
仅锁定id=1这一行;但对非唯一索引范围查询:
-- 非唯一索引范围查询,加临键锁(记录锁 + 间隙锁)
SELECT * FROM orders WHERE amount > 100 FOR UPDATE;
不仅锁定现有满足条件的记录,还锁定其右侧间隙(如(100, +∞)),防止其他事务插入新记录——即防止幻读。
MVCC与锁的协同关系明确:
- 快照读(如普通
SELECT):优先使用MVCC,读取历史版本,无锁,依赖undo log构建版本链; - 当前读(如
SELECT ... FOR UPDATE、UPDATE、DELETE):读取最新已提交版本,并加锁,确保写操作的独占性。 - 写操作:必须加锁,确保数据修改的独占性
- 冲突检测:通过锁机制防止写-写冲突
- 版本管理:通过undo log维护多版本数据
注意:InnoDB的乐观锁(如基于版本号的CAS操作)是应用层实现,非数据库内核机制。
意向锁作用示例:
-- 事务1:加行级X锁,自动在表上加IX锁
SELECT * FROM users WHERE id=1 FOR UPDATE;
-- 事务2:尝试加表级S锁
LOCK TABLES users READ;
-- 由于检测到表上存在IX锁,直接判断冲突,无需逐行检查
-- 大幅提升加锁效率
这种设计使InnoDB在保证强一致性的同时,最大化读吞吐能力。
2.4 一致性(Consistency)
一致性是ACID的最终目标,需由原子性、持久性、隔离性共同保障,并依赖数据库的完整性约束体系。
完整性约束包括:
- 实体完整性:主键唯一且非空;
- 参照完整性:外键关联一致性;
- 域完整性:数据类型、非空、检查约束;
- 用户定义完整性:通过触发器、存储过程实现业务规则。
一致性保障存在三个层次:
- 语句级:单条SQL执行具有原子性;
- 事务级:整个事务提交/回滚具有原子性;
- 系统级:崩溃恢复后数据库恢复至一致状态。
在Java应用中,@Transactional注解本身只保障事务级一致性,若业务规则涉及跨库、跨服务,则需引入分布式事务方案保障全局一致性:
- XA协议:MySQL原生支持,但性能较差,适合对一致性要求极高的场景
- Seata AT/TCC模式:适合微服务架构下的分布式事务
- Saga模式:适合长事务场景,通过补偿机制实现最终一致性
- 消息队列:如RocketMQ事务消息,实现最终一致性,避免强一致性开销
生产建议:在微服务架构中,优先使用Saga模式或消息队列实现最终一致性,避免分布式强一致性带来的性能开销和可用性降低。
三、事务隔离级别与并发问题深度分析
3.1 四种隔离级别的技术对比
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 实现机制 |
|---|---|---|---|---|
| 读未提交 | ❌ | ❌ | ❌ | 无锁读取最新数据 |
| 读已提交(RC) | ✅ | ❌ | ❌ | MVCC+行锁,语句级快照 |
| 可重复读(RR) | ✅ | ✅ | ⚠️ 部分避免 | MVCC+间隙锁,事务级快照 |
| 串行化 | ✅ | ✅ | ✅ | 表级锁,完全串行化 |
符号说明:✅表示避免该问题,❌表示可能发生,⚠️表示部分避免。
重要提示:
- RR级别通过Next-Key Lock避免多数幻读,但对纯快照读(如无FOR UPDATE的范围查询)仍可能发生
- 串行化级别性能最低,仅用于极端一致性需求场景
- MySQL默认RR级别,与Oracle(默认RC)不同,需特别注意
- 使用
INSERT ... ON DUPLICATE KEY UPDATE时仍可能出现幻读,需显式通过SELECT ... FOR UPDATE加锁
3.2 并发问题技术详解
脏读(Dirty Read)
脏读发生在一个事务读取了另一个未提交事务修改的数据。
技术场景再现:
-- 时间点T1:事务A开始并修改数据
START TRANSACTION;
UPDATE products SET stock = stock - 10 WHERE id = 1;
-- 此时stock从100变为90,但未提交
-- 时间点T2:事务B读取数据
START TRANSACTION;
SELECT stock FROM products WHERE id = 1;
-- 事务B读取到90(脏数据)
-- 时间点T3:事务A回滚
ROLLBACK;
-- stock恢复为100,但事务B已基于90进行了业务处理
-- 时间点T4:事务B提交
COMMIT;
-- 此时事务B使用了不存在的数据90
注意:将隔离级别提升至RC或者更高可以解决。
不可重复读(Non-repeatable Read)
在同一事务中,多次读取同一数据返回不同结果。
技术场景深度分析:
-- 事务A:银行余额验证事务
START TRANSACTION;
-- 第一次查询:检查余额
SELECT balance INTO @balance1 FROM accounts WHERE id = 1;
-- 返回1000元,事务A基于此进行业务逻辑判断
-- 此时事务B执行并提交:
START TRANSACTION;
UPDATE accounts SET balance = balance - 200 WHERE id = 1;
COMMIT;
-- 余额变为800元
-- 事务A第二次查询:验证余额一致性
SELECT balance INTO @balance2 FROM accounts WHERE id = 1;
-- 返回800元,与第一次查询结果不一致!
注意:RC级别下此现象正常;RR级别则通过事务级ReadView避免——首次查询即生成快照,后续查询复用,结果一致。
幻读(Phantom Read)
在同一事务中,多次查询返回不同的记录集合。
技术场景深度解析:
-- 事务A:生成月度销售报告
START TRANSACTION;
-- 第一次查询:获取当前订单数量
SELECT COUNT(*) INTO @order_count FROM orders
WHERE order_date >= '2025-01-01' AND order_date < '2025-02-01';
-- 返回500个订单
-- 此时事务B插入新订单并提交:
START TRANSACTION;
INSERT INTO orders(user_id, amount, order_date)
VALUES (1001, 500.00, '2025-01-15');
COMMIT;
-- 事务A第二次查询:验证数据一致性
SELECT COUNT(*) INTO @order_count2 FROM orders
WHERE order_date >= '2025-01-01' AND order_date < '2025-02-01';
-- 返回501个订单,出现幻行!
**注意:**RR级别下,对范围查询加临建锁(Next-Key Lock)可以防止幻读
-- 使用SELECT ... FOR UPDATE加临键锁
START TRANSACTION;
SELECT COUNT(*) FROM orders
WHERE order_date >= '2025-01-01' AND order_date < '2025-02-01'
FOR UPDATE;
-- 此时(‘2025-01-01’, ‘2025-02-01’)区间被锁定,插入被阻塞
四、MVCC(多版本并发控制)技术深度解析
4.1 MVCC架构核心组件
数据行物理结构:
| 字段名 | 大小 | 说明 |
|---|---|---|
| DB_ROW_ID | 6字节 | 行ID(无主键时生成) |
| DB_TRX_ID | 6字节 | 最近修改事务ID |
| DB_ROLL_PTR | 7字节 | 回滚指针指向undo log |
| 用户数据 | 可变 | 实际存储的用户数据 |
当一行被多次修改时,各版本通过DB_ROLL_PTR形成版本链,并由undo log保存旧值:
graph LR
A[当前版本] --> B[旧版本1]
B --> C[旧版本2]
C --> D[旧版本N]
D --> E[初始版本]
A -->|DB_ROLL_PTR| F[undo log记录1]
B -->|DB_ROLL_PTR| G[undo log记录2]
C -->|DB_ROLL_PTR| H[undo log记录N]
该结构使InnoDB能为不同事务提供各自时间点的数据快照,实现无锁读。
Purge机制:旧版本数据通过background purge线程异步清理。可通过
SHOW ENGINE INNODB STATUS中的History list length监控undo版本链长度,数值过大(如>1000000)表明存在长事务或purge延迟,会导致查询性能下降。
4.2 ReadView机制核心技术
执行快照读时,InnoDB生成一个ReadView结构,作为可见性判断依据:
struct ReadView {
trx_id_t low_limit_id; // 高水位:大于等于此值的事务不可见
trx_id_t up_limit_id; // 低水位:小于此值的事务可见
trx_id_t creator_trx_id; // 创建ReadView的事务ID
ids_t ids; // 活跃事务ID列表
// 其他管理字段...
};
可见性判断算法详细流程:
具体判断示例:
-- 假设当前ReadView:
-- low_limit_id = 150, up_limit_id = 100,
-- active_trx_ids = [110, 120, 130], creator_trx_id = 140
-- 判断不同事务ID的可见性:
-- trx_id = 90: < up_limit_id(100) → 可见
-- trx_id = 110: 在活跃列表中 → 不可见
-- trx_id = 125: 不在活跃列表,但 < low_limit_id → 可见
-- trx_id = 160: >= low_limit_id → 不可见
4.3 不同隔离级别的ReadView管理
READ COMMITTED下,每次SELECT语句都创建新ReadView:
SELECT * FROM table WHERE ... ; -- 创建ReadView1
-- 其他事务提交修改...
SELECT * FROM table WHERE ... ; -- 创建ReadView2,可能看到新数据
因此可能出现不可重复读。
REPEATABLE READ下,事务中首次SELECT创建ReadView,后续查询复用:
START TRANSACTION;
SELECT * FROM table WHERE ... ; -- 创建ReadView
-- 其他事务提交修改...
SELECT * FROM table WHERE ... ; -- 复用ReadView,结果不变
从而实现可重复读,这是MySQL默认级别,与Oracle等数据库不同,需特别注意。
五、InnoDB锁机制全面深入解析
5.1 锁兼容性矩阵
锁的兼容性决定了并发能力。核心四种锁的兼容关系如下:
| 请求锁类型 \ 当前持有锁 | 共享锁(S) | 排他锁(X) | 意向共享锁(IS) | 意向排他锁(IX) | 说明 |
|---|---|---|---|---|---|
| 共享锁(S) | ✅ 兼容 | ❌ 冲突 | ✅ 兼容 | ❌ 冲突 | 读-读兼容,读-写冲突 |
| 排他锁(X) | ❌ 冲突 | ❌ 冲突 | ❌ 冲突 | ❌ 冲突 | 写操作完全独占 |
| 意向共享锁(IS) | ✅ 兼容 | ❌ 冲突 | ✅ 兼容 | ✅ 兼容 | 表级意向锁 |
| 意向排他锁(IX) | ❌ 冲突 | ❌ 冲突 | ✅ 兼容 | ✅ 兼容 | 表级意向锁 |
意向锁是表级锁,用于快速判断表中是否存在行锁,避免逐行检查,提升加锁效率。
其他重要锁类型:
- AUTO-INC锁:用于自增列,MySQL 8.0默认使用轻量级互斥模式(
innodb_autoinc_lock_mode=2) - MDL(Metadata Lock):DDL操作时加的元数据锁,是DDL阻塞DML的常见原因,可通过
performance_schema.metadata_locks监控
5.2 行级锁技术深度解析
记录锁(Record Lock)
基于主键或唯一索引的等值查询,退化为记录锁:
-- 基于主键或唯一索引的等值查询
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- 加锁过程:
-- 1. 在id=1的索引记录上加X锁
-- 2. 如果存在二级索引,在相应索引项也加锁
-- 3. 锁住具体的数据行
间隙锁(Gap Lock)
防止幻读的关键;非唯一索引等值查询或范围查询会触发:
-- 防止幻读的关键技术
SELECT * FROM orders WHERE amount BETWEEN 100 AND 200 FOR UPDATE;
-- 加锁范围:
-- 1. 锁定amount在(100, 200)开区间内的所有间隙
-- 2. 防止其他事务插入amount在此范围内的新记录
-- 3. 允许更新区间外的现有记录
间隙锁的特殊情况:
- 唯一索引等值查询:退化为记录锁,不加间隙锁
- 非唯一索引等值查询:加间隙锁,防止幻读
- 范围查询:加临键锁(Next-Key Lock)
临键锁(Next-Key Lock)
记录锁 + 间隙锁的组合,默认用于RR级别下的范围查询:
-- 记录锁 + 间隙锁的组合
SELECT * FROM products WHERE price > 100 FOR UPDATE;
-- 加锁效果:
-- 1. 对price>100的所有现有记录加记录锁
-- 2. 对price>100的范围加间隙锁
-- 3. 综合防止幻读和保证当前读的一致性
5.3 死锁检测与处理机制
InnoDB使用wait-for graph(等待图)算法检测死锁,时间复杂度O(n),高并发时可能有一定开销。
死锁避免:
-- 1. 统一数据访问顺序(如始终先操作user表,再order表)
-- 2. 使用较低隔离级别降低锁强度
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 3. 设置锁等待超时,避免无限等待
SET SESSION innodb_lock_wait_timeout = 30;
-- 4. 避免长事务(不在@Transactional方法中调用HTTP、复杂计算)
-- 5. 死锁监控与排查
-- 使用SHOW ENGINE INNODB STATUS查看最近死锁日志
SHOW ENGINE INNODB STATUS\G
-- 或使用Percona Toolkit的pt-deadlock-logger进行持续监控
-- 6. 高并发场景可考虑禁用死锁检测(不推荐)
-- SET GLOBAL innodb_deadlock_detect = OFF;
-- 禁用后需依赖innodb_lock_wait_timeout超时机制
六、事务日志系统协同工作机制
6.1 三日志协同工作原理
一个事务的完整生命周期涉及undo log、redo log、binlog三者协同:
关键日志协同时序:
- undo log先行:在数据修改前记录旧值
- redo log prepare:事务操作过程中记录重做日志
- binlog写入:事务提交时写入二进制日志
- redo log commit:确认事务提交完成
- 异步刷盘:由后台线程负责数据页持久化
6.2 undo log详细技术实现
undo log记录结构:
struct undo_log_record {
uint64 trx_id; // 事务ID
uint64 roll_ptr; // 回滚指针
uint16 type; // 操作类型:INSERT/UPDATE/DELETE
uint64 table_id; // 表ID
byte[] old_data; // 旧数据
byte[] new_data; // 新数据(UPDATE时)
// 其他管理字段...
};
undo log类型分类:
- INSERT undo log:INSERT操作的逆操作,事务提交后可立即删除
- UPDATE undo log:UPDATE/DELETE操作的逆操作,需要支持MVCC,事务提交后不能立即删除
6.3 redo log两阶段提交技术细节
两阶段提交协议:
sequenceDiagram
participant T as 事务管理器
participant R as redo log
participant B as binlog
participant S as 存储引擎
T->>R: 1. redo log prepare
R-->>T: prepare完成
T->>B: 2. 写binlog
B-->>T: binlog写入成功
T->>R: 3. redo log commit
R-->>T: commit完成
T->>S: 4. 事务提交确认
崩溃恢复处理:
- 情况1:只有redo log prepare,binlog未写 → 回滚事务
- 情况2:有redo log prepare,binlog已写 → 提交事务
- 情况3:redo log commit完成 → 事务已提交,无需处理
该机制确保:主库崩溃后恢复的数据状态,与从库通过binlog重放的结果完全一致。
高可用配置(双1配置):在MySQL主从复制中,启用
sync_binlog=1与innodb_flush_log_at_trx_commit=1是高可用底线,确保主从数据一致性。
日志类型区别:
- binlog:逻辑日志,记录SQL语句或行变更,用于主从复制和数据恢复
- redo log:物理日志,记录数据页的物理修改,用于崩溃恢复
七、完整事务执行流程技术深度解析
7.1 复杂事务示例:订单创建
-- 开始分布式事务
START TRANSACTION;
-- 阶段1:库存检查与预留
SELECT stock INTO @current_stock FROM inventory WHERE product_id = 1001;
IF @current_stock >= 5 THEN
UPDATE inventory SET stock = stock - 5, reserved = reserved + 5
WHERE product_id = 1001;
ELSE
ROLLBACK;
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '库存不足';
END IF;
-- 阶段2:创建订单主记录
INSERT INTO orders (user_id, total_amount, status)
VALUES (123, 500.00, 'pending');
SET @order_id = 5932453;
-- 阶段3:创建订单明细
INSERT INTO order_items (order_id, product_id, quantity, price)
VALUES (@order_id, 1001, 5, 100.00);
-- 阶段4:扣减用户余额
UPDATE accounts SET balance = balance - 500 WHERE user_id = 123;
-- 阶段5:记录审计日志
INSERT INTO audit_log (action, user_id, details)
VALUES ('create_order', 123, CONCAT('创建订单:', @order_id));
-- 提交事务
COMMIT;
技术要点:
- 所有操作在一个事务内,保证原子性;
- 每步业务校验(如库存检查)前置,避免无效操作;
- 日志记录放在最后,减少锁持有时间。
7.2 事务回滚的完整流程
当异常发生时,回滚流程如下:
sequenceDiagram
participant C as 客户端
participant SE as SQL执行器
participant TM as 事务管理器
participant UL as Undo日志管理器
participant LM as 锁管理器
participant BP as Buffer Pool
participant RL as Redo日志
C->>SE: 执行DML语句
SE->>TM: 检查事务状态
TM-->>SE: 事务正常
SE->>SE: 执行遇到异常
alt 违反约束
SE->>SE: 数据约束检查失败
else 死锁检测
SE->>LM: 检测到死锁
LM-->>SE: 选择牺牲者
else 超时
SE->>TM: 事务超时检测
end
SE->>TM: 报告异常,请求回滚
TM->>TM: 设置事务状态为ROLLBACK
TM->>UL: 开始回滚流程
Note over UL,TM: 阶段1:逆向回滚数据操作
UL->>UL: 获取undo log链
loop 针对每个undo log记录
UL->>UL: 读取undo记录类型和内容
alt type = UPDATE
UL->>BP: 应用更新回滚
BP->>BP: 恢复旧数据版本
else type = INSERT
UL->>BP: 删除新插入记录
BP->>BP: 移除数据页记录
else type = DELETE
UL->>BP: 重新插入被删记录
BP->>BP: 恢复完整数据行
end
UL->>UL: 标记undo记录为已回滚
end
Note over TM,LM: 阶段2:释放资源
TM->>LM: 请求释放所有锁
LM->>LM: 释放行级锁
LM->>LM: 释放表级锁
LM-->>TM: 锁释放完成
TM->>TM: 清理事务上下文
TM->>RL: 写入回滚记录
Note over TM,C: 阶段3:返回结果
TM->>SE: 回滚完成
SE-->>C: 返回错误和回滚结果
Note right of C: 收到错误信息:<br/>ERROR/ROLLBACK COMPLETE
回滚是逆序执行undo log中记录的逆操作,每步恢复一个历史版本,最终使数据回归事务开始前状态。
八、总结
又是没有大厂约面日子😣😣😣,小编还在找实习的路上,这篇文章是我的笔记汇总整理。
参考文献
- MySQL 8.0 Reference Manual - InnoDB Storage Engine
- MySQL 8.0 Reference Manual - InnoDB Locking and Transaction Model
- 《高性能MySQL》第四版
- 《MySQL技术内幕:InnoDB存储引擎》