事务的概念和特性
对于事务的四个特性 其中原子性、一致性、持久性依赖redo log和undo log实现,隔离性依赖锁机制和MVCC来实现
redo log 概述
redo log保证ACID中的D,即持久性。
为什么redo log也需要持久化到磁盘? 因为redo log的使命是在任何情况下都能保证已提交的事务(其日志在提交时已持久化)的数据绝不会丢失,为了应对极端情况,如系统断电(数据库崩溃)时或者数据刷盘时遇到故障,可以通过重放redo log日志实现数据的恢复。所以redo log必须先(Write-AHead Logging)持久化。
另一方面,系统通过redo log的快速持久化(顺序IO)来保证事务的快速响应(在用户执行
COMMIT
的瞬间,立即、可靠地确认这个事务已经成功)。而不是每次等事务真正的数据修改持久化(随机IO操作缓慢,耗时较长)后才响应。
undo log 概述
实现事务的原子性(Atomicity)和回滚(Rollback)
事务是一个不可分割的工作单位,事务中的操作要么全部成功,要么全部失败。
如何实现回滚(原子性)?
当一个事务对数据进行修改时(INSERT, UPDATE, DELETE),InnoDB 不仅会产生 Redo Log,还会产生 Undo Log。
- 对于
UPDATE
和DELETE
操作:
- 生成内容:Undo Log 会记录修改前数据的旧版本(拷贝旧记录到 Undo Log)。
- 目的:如果需要回滚,就可以根据这个记录,将数据恢复到修改前的状态。它逻辑上是执行一个逆向的
UPDATE
(将新值改回旧值)或INSERT
(将删除的行再插回去)。- **什么情况下可删除?**不再被MVCC数据版本引用的时候。
- 对于
INSERT
操作:
- 生成内容:Undo Log 会记录新插入行的 主键信息。
- 目的:如果需要回滚,直接根据这个主键信息执行一个
DELETE
操作即可。- **什么情况下可删除?**事务提交后。
回滚过程: 当执行
ROLLBACK
时,InnoDB 会从 Undo Log 中读取相应的记录,并执行逆向操作来撤销本事务所做的修改。撤销完成后,该事务生成的 Undo Log 也就完成了使命。实现多版本并发控制(MVCC - Multi-Version Concurrency Control)
MVCC:这是 MySQL 实现高并发的重要机制。它通过保存数据的多个历史版本,使得读操作(
SELECT
)不会阻塞写操作(UPDATE
,DELETE
),写操作也不会阻塞读操作,极大地提升了数据库的并发性能。如何支实现MVCC(实现一致性读)?
这是 Undo Log 更精妙的用法。我们通过一个经典的“读-写”并发场景来看:
- 假设事务 A(事务ID=100)开始后,修改了行
R
,将name
从"Alice"
改为"Bob"
。这个修改过程同时写入了 Redo Log 和 Undo Log。- 此时,事务 B(事务ID=101)开始,它希望读取行
R
。- 为了保证事务 B 能看到一个一致的快照(Read View),InnoDB 不会直接让它读取当前最新的值(
"Bob"
),因为事务 A 可能还没提交,这个数据是“脏”的。- InnoDB 会通过行记录上的一个隐藏字段(
DB_ROLL_PTR
,回滚指针)顺藤摸瓜。这个指针指向了写入 Undo Log 中的上一个历史版本。- 事务 B 沿着这个指针链,找到了事务 A 修改之前的版本(
name = "Alice"
),并将这个旧版本数据返回给用户。- 这样,事务 B 实现了一次非阻塞的快照读(一致性读),而事务 A 的写操作也没有被阻塞。
undo log实现MVCC的复杂场景
场景 setup
我们有一张简单的表
accounts
:
id name balance DB_TRX_ID DB_ROLL_PTR 1 张三 1000.00 80 0x7a11c0
DB_TRX_ID
(隐藏字段):最近一次修改该行数据的事务ID。DB_ROLL_PTR
(隐藏字段):指向该行上一个历史版本在 Undo Log 中的地址指针。初始事务状态:
- 事务
TRX_80
已提交,它创建了这条初始数据。
复杂操作序列
当前读和快照读
复杂操作序列举例
现在,三个事务按以下顺序开始操作,它们的开始时机和隔离级别决定了它们能看到什么。
时间点 T1:
TRX_90
开始 (这是一个写事务,假设隔离级别为READ COMMITTED
或REPEATABLE READ
)TRX_90
执行UPDATE accounts SET balance = balance - 100 WHERE id = 1;
(将余额减去100)此时,InnoDB 的操作:
- 它不是直接覆盖原数据,而是先将当前行的数据拷贝到 Undo Log 中。Undo Log 中现在有一条记录:
[Undo Record: id=1, name='张三', balance=1000.00, modified_by_TRX=80]
- 然后它才更新内存中
accounts
表的数据行:
balance = 900.00
DB_TRX_ID = 90
(更新为当前事务ID)DB_ROLL_PTR
指向刚刚创建的 Undo Log 记录地址,比如0x8b22d1
。- 同时,这个修改也会记录到 Redo Log 以保证持久性。
此时,数据库中有两个版本的数据:
- 当前版本:
(id=1, balance=900, TRX_ID=90, ROLL_PTR -> 0x8b22d1)
- 历史版本:
(id=1, balance=1000, TRX_ID=80)
(存储在 Undo Log 中,由0x8b22d1
指向)时间点 T2:
TRX_91
开始 (这是一个只读事务,隔离级别为REPEATABLE READ
)TRX_91
执行SELECT balance FROM accounts WHERE id = 1;
它要读取数据了!MVCC 魔法开始:
- InnoDB 为
TRX_91
创建一个 Read View(一致性视图)。这个视图决定了TRX_91
能看到哪些事务的修改。
- 关键规则:它只能看到所有事务ID <= 91` 且已经提交的事务所做的修改。
- 在 T2 时刻,
TRX_90
(ID=90) 还未提交。- InnoDB 从最新的数据行开始检查:
- 最新数据的
DB_TRX_ID = 90
。TRX_91
的 Read View 检查:90 < 91,但事务90未提交 -> 此版本对当前事务不可见。- InnoDB 顺着回滚指针
ROLL_PTR (0x8b22d1)
找到 Undo Log 中的历史版本。- 检查历史版本:
DB_TRX_ID = 80
。
TRX_91
的 Read View 检查:80 < 91,且事务80已提交 -> 此版本对当前事务可见!- 因此,
TRX_91
读取到的balance
是1000.00
,而不是最新的900.00
。它完美地避免了对未提交数据的脏读。时间点 T3:
TRX_90
执行COMMIT;
提交事务。时间点 T4:
TRX_92
开始 (另一个只读事务,隔离级别为READ COMMITTED
)TRX_92
执行SELECT balance FROM accounts WHERE id = 1;
MVCC 再次工作 (不同隔离级别的差异):
- InnoDB 为
TRX_92
创建一个新的 Read View。
- 对于
READ COMMITTED
,它的规则是:只看到在本语句执行前已经提交的事务。- 在 T4 时刻,
TRX_90
(ID=90) 已经提交。- InnoDB 找到最新数据行:
DB_TRX_ID = 90
。TRX_92
的 Read View 检查:90 < 92,且事务90已提交 -> 此版本对当前事务可见。- 因此,
TRX_92
读取到的balance
是900.00
。它读到了已提交的最新数据。时间点 T5:
TRX_91
再次执行SELECT balance FROM accounts WHERE id = 1;
(同一个事务内的第二次查询)MVCC 的核心魅力 (可重复读):
TRX_91
在 T2 时刻第一次查询时,就已经生成了它的 Read View。- 对于
REPEATABLE READ
隔离级别,一个事务只在第一次执行查询时创建 Read View,后续所有查询都复用这个视图。- 因此,即使在 T5 时刻
TRX_90
已经提交,TRX_91
的 Read View 规则依然不变:它依然看不到 TRX_90 的修改(因为在它创建视图时,TRX_90 未提交)。- InnoDB 再次沿着版本链查找,找到的依然是 Undo Log 中那个由
TRX_80
创建的、已提交的版本。- 因此,
TRX_91
第二次查询读取到的balance
依然是1000.00
。这就实现了“可重复读”,即同一事务内多次读取同一数据,结果是一致的。
undo log 总结
Undo Log 在 MVCC 中的复杂运用体现在:
- 构建版本链:每次修改都记录旧数据到 Undo Log,并通过回滚指针串联起来。
- 提供历史快照:当某个事务需要读取时,InnoDB 遍历这个版本链,并根据该事务的 Read View 的可见性规则,为它找到一个合适的、可见的历史版本。
- 实现不同隔离级别:
READ COMMITTED
:每次查询都生成新 Read View,所以能看到最新已提交的版本。REPEATABLE READ
:第一次查询生成 Read View 后不再改变,所以总是看到同一个历史版本,实现可重复读。- 清理机制:这些 Undo Log 版本不会永远存在。当没有任何现存的事务需要看到某个历史版本时(即没有比它更老的事务活跃时),这个版本的 Undo Log 就可以被 Purge 线程安全地清理掉。