事务的概念和特性


对于事务的四个特性 其中原子性、一致性、持久性依赖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.00DB_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 线程安全地清理掉。