详解Innodb一次更新事物的执行过程
InnoDB 的事务更新过程是一个精心设计的机制,确保数据的 ACID 特性(原子性、一致性、隔离性和持久性)。下面我将详细解析一次完整的事务更新过程:
1. 事务启动
-
分配事务 ID:事务开始时,系统为事务分配一个唯一的事务 ID(
trx_id
)。 -
创建 ReadView(隔离级别为 Repeatable Read 时):事务首次读操作时会生成一个
ReadView
,用于判断数据的可见性(基于活跃事务列表)。
2. 执行 UPDATE 操作
2.1 定位目标数据行
-
通过索引查找数据:
-
根据
WHERE
条件,通过聚簇索引(主键索引)或二级索引或者全表扫描找到目标数据行。 -
如果是二级索引,需要回表到聚簇索引获取完整数据行。
-
2.2 加锁
-
行级锁(X 锁):
-
对目标数据行加 排他锁(X 锁),阻止其他事务修改或加锁。
-
如果隔离级别是可重复读,还会对相邻的间隙加 间隙锁(Gap Lock),防止幻读。
-
2.3 生成 Undo Log
-
记录旧版本数据:
-
将修改前的数据(旧版本)写入 undo log,用于事务回滚和 MVCC。
-
数据行的隐藏字段
roll_pointer
指向 undo log 中的旧版本记录,形成版本链。
-
2.4 修改数据页
-
更新 Buffer Pool:
-
在内存的 Buffer Pool 中修改数据页,生成新的数据版本。
-
新数据行的隐藏字段
trx_id
设置为当前事务的 ID。
-
2.5 生成 Redo Log
-
记录物理修改:
-
将数据页的物理修改(如页号、偏移量、修改内容)写入 redo log buffer。
-
redo log 是顺序写入的物理日志,用于崩溃恢复。
-
3. 事务提交(COMMIT)
3.1 刷写 Redo Log
-
保证持久性:
-
将 redo log buffer 中的日志刷盘(
fsync
)。 -
通过参数
innodb_flush_log_at_trx_commit
控制刷盘策略:-
=1
:每次提交都刷盘(默认,保证持久性)。 -
=0
:每秒刷盘(可能丢失 1 秒数据)。 -
=2
:写入 OS 缓存,不保证立即刷盘。
-
-
3.2 释放锁
-
行锁与间隙锁:
-
释放事务持有的所有行级锁和间隙锁。
-
其他被阻塞的事务可以继续执行。
-
3.3 清理资源
-
标记事务结束:
-
将事务状态标记为已提交,
ReadView
失效(后续事务不再依赖该事务的可见性)。 -
延迟清理 undo log(可能被其他事务的 MVCC 读操作依赖)。
-
3.4 写入 Binlog(可选)
-
主从复制:
-
如果启用了二进制日志(binlog),在提交阶段通过 两阶段提交(2PC) 保证 redo log 和 binlog 的一致性。详解Mysql redo log与binlog的两阶段提交(2PC)
-
4. 事务回滚(ROLLBACK)
-
反向执行 Undo Log:
-
根据 undo log 中的旧版本数据,将数据页恢复到事务开始前的状态。
-
-
释放锁:
-
释放所有行锁和间隙锁。
-
-
清理事务状态:
-
标记事务为已回滚,删除相关的 undo log(若无其他事务依赖)。
-
5. 关键机制详解
5.1 锁机制
-
行锁(Record Lock):
-
确保同一行数据不会被多个事务同时修改。
-
通过
SHOW ENGINE INNODB STATUS
可查看锁信息。
-
-
间隙锁(Gap Lock):
-
在可重复读隔离级别下,锁定索引范围,防止其他事务插入数据(解决幻读)。
-
5.2 Redo Log 与 Undo Log
-
Redo Log:
-
物理日志,记录页的修改。
-
顺序写入,崩溃恢复时重放未刷盘的修改。
-
-
Undo Log:
-
逻辑日志,记录反向 SQL(如
UPDATE
对应DELETE
)。 -
支持事务回滚和 MVCC 的版本链。
-
5.3 MVCC 与版本链
-
数据版本链:
-
每条记录的隐藏字段
trx_id
和roll_pointer
指向 undo log 中的旧版本。
-
-
ReadView:
-
包含活跃事务 ID 列表(
m_ids
),用于判断数据版本是否可见。 -
可见性规则:
-
如果数据行的
trx_id
< 最小活跃事务 ID,则可见。 -
如果数据行的
trx_id
在活跃事务列表中,不可见。 -
如果数据行的
trx_id
是当前事务自身,可见。
-
-
6. 示例:一次 UPDATE 的完整流程
假设执行以下 SQL:
UPDATE user SET name = 'Alice' WHERE id = 1;
-
事务启动:
-
分配
trx_id=100
,生成ReadView
(活跃事务列表为空)。
-
-
查找数据行:
-
通过主键索引找到
id=1
的数据行,当前trx_id=90
(已提交)。
-
-
加锁:
-
对
id=1
的行加 X 锁,并加间隙锁(如果需要)。
-
-
生成 Undo Log:
-
记录旧值
name='Bob'
,roll_pointer
指向该 undo log。
-
-
修改数据页:
-
在 Buffer Pool 中将
name
改为Alice
,trx_id
设置为100
。
-
-
生成 Redo Log:
-
记录页修改的物理操作到 redo log buffer。
-
-
事务提交:
-
刷写 redo log 到磁盘,释放锁,标记事务完成。
-
7. 性能优化点
-
减少锁冲突:
-
使用索引优化查询条件,缩小加锁范围。
-
-
合理设置隔离级别:
-
在 Read Committed 下,间隙锁较少,但可能牺牲一致性。
-
-
批量提交:
-
合并多个操作为一个事务,减少 redo log 刷盘次数。
-
-
调整 redo log 大小:
-
增大
innodb_log_file_size
减少刷盘频率。
-