事务执行流程
START TRANSACTION;
UPDATE accounts
SET balance = balance - 100
WHERE id = 1;
UPDATE accounts
SET balance = balance + 100
WHERE id = 2;
COMMIT;
以转账事务为例,看下 MySQL 各组件是如何协同工作的:
- 客户端发送
START TRANSACTION命令给 MySQL Server - MySQL Server 收到后,通知 InnoDB 开启事务,并分配事务 ID,
- 客户端发送第一条 UPDATE 语句,MySQL Server 将 UPDATE 语句交给 InnoDB 执行
- InnoDB 查看 UPDATE 语句所涉及的数据行是否在内存的 Buffer Pool 中,如果不在,则将数据行所在的数据页加载到 Buffer Pool 中。
- 修改数据之前,先对需要修改的数据行上锁。
- 写 undo log。将修改前的旧值写到 undo log 中
- 修改 Buffer Pool 中相关数据页的数据行,并将数据页标记为脏页
- 写 redo log。把对数据行的修改写入 redo log 中
- 客户端发送第二条 UPDATE 语句,重复 4 - 8 中的步骤
- 客户端发送
COMMIT;指令。MySQL Server 收到后,与 InnoDB 开启两阶段提交 - MySQL Server 通知 InnoDB 在 redo log 中将事务标记为 PREPARE 状态
- InnoDB 成功写入 PREPARE 状态后,给 MySQL Server 回复 ACK
- MySQL Server 收到 PREPARE 的 ACK 后,会把本次事务对数据行的修改写入到 bin log 中
- MySQL Server 成功写入 bin log 后,会通知 InnoDB 将事务标记为 COMMITED 状态
- InnoDB 成功写入 COMMITED 状态后,给 MySQL Server 回复 ACK
- MySQL Server 收到 InnoDB 的 ACK 后,通知客户端事务提交成功。
- 后台线程定期或强制将 Buffer Pool 中部分脏页写入到磁盘的数据页中
具体的流程图如下:
sequenceDiagram
autonumber
participant C as Client
participant S as MySQL Server
participant I as InnoDB
participant BP as InnoDB Buffer Pool
participant UL as Undo Log
participant RL as Redo Log
participant BL as Binlog
participant CK as Checkpoint & Flusher
C->>S: START TRANSACTION
S->>I: trx_begin()
C->>S: UPDATE accounts ...
S->>I: exec_update()
I->>BP: 读/加载数据页
I->>BP: 对相关数据行加锁
I->>UL: 写 undo 记录 (旧版本)
I->>BP: 写数据页,标记为脏页
I->>RL: 写 redo 日志到 log buffer
C->>S: COMMIT
Note over S,I: 事务提交,两阶段提交开始
S->>I: prepare()
I->>RL: 写 PREPARE 记录<br/>并刷 redo 到磁盘 (fsync)
I-->>S: prepare OK
S->>BL: 写 binlog 事件 + XID
BL-->>S: 刷 binlog 到磁盘 (fsync)
S->>I: commit()
I->>RL: 写 COMMIT 记录到 redo
I->>BP: 释放锁 / 标记事务已提交
I-->>S: commit OK
S-->>C: COMMIT 成功返回
par 后台线程
loop
CK->>BP: 选择部分脏页
BP->>I: 写回数据文件
I->>CK: 更新 checkpoint LSN
end
end
Bin log 和 Redo log 的两阶段提交
- 客户端通知 MySQL Server 提交事务
- MySQL Server 通知 InnoDB 将本次事务的所有变更提交到 redo log,并将事务的 PREPARE 状态也写入 redo log,PREPARE 状态会带有 XID(事务 ID)。变更全部写入成功后 InnoDB 给 MySQL Server 发送 ACK
- MySQL Server 收到 InnoDB 的 ACK 后,将本次事务的所有变更以及 XID(事务 ID) 写入到 bin log 中。写入成功后,MySQL Server 给 InnoDB 发送 ACK
- InnoDB 收到 MySQL Server 的 ACK 后,将事务的 COMMITED 状态写入 redo log 中,COMMITED 状态会带有 XID(事务 ID)。成功写入 redo log 后,InnoDB 给 MySQL Server 发送 ACK
- MySQL Server 收到 InnoDB 的 ACK 后,给客户端发送事务成功提交的消息。
具体的流程如下:
sequenceDiagram
autonumber
participant App as Client
participant S as MySQL Server
participant I as InnoDB
participant RL as Redo Log
participant BL as Binlog
App->>S: COMMIT
Note over S,RL: 阶段 1:InnoDB prepare
S->>I: prepare()
I->>RL: 写本事务所有修改的 redo<br/>并写 PREPARE 记录
RL-->>I: fsync 持久化成功
I-->>S: prepare OK (事务进入 prepared 状态)
Note over S,BL: 阶段 2 上半:写 binlog
S->>BL: 写 binlog event + XID
BL-->>S: fsync binlog 成功
Note over S,RL: 阶段 2 下半:InnoDB commit
S->>I: commit()
I->>RL: 写 COMMIT 记录到 redo<br/>(通常不强制 fsync)
I-->>S: commit OK
S-->>App: COMMIT 成功返回
在两阶段提交中,以 bin log 中事务的状态为准。例如启动 MySQL 时, bin log 中事务 10101 的状态是 COMMITED,但是在 redo log 中,事务 10101 的状态还是 PREPARE。此时以 bin log 中的事务状态为准,需要在 InnoDB 中进行"补提交",避免 bin log 中有,但是引擎层没有的情况。对于 bin log 中事务状态是非提交的情况,一律回滚事务。