Appearance
MySQL ACID实现机制
本文介绍事务四大特性ACID的实现机制。
1. Durability-持久性
持久性的定义:一旦事务提交,其所做的修改将永久保存在数据库中,即使系统发生崩溃或电源故障也不会丢失。
实现持久性的原理:Redo log(重做日志) + Doublewrite Buffer(双写缓冲区)
我们以下面的例子来说明实现持久性的机制:
sql
begin; -- 开启事务
update t set name='a' where id=1; -- 修改数据,id是主键
commit;- 首先手动开启事务,那么系统不会自动提交DML操作;
- 然后执行
update语句:- 首先通过主键索引找到数据页,如果该页不在缓冲池中,则将该页加载进缓冲池中;
- 然后在该页上执行修改操作,将
name改为‘a’; - 将数据页标记为脏页;
- 记录修改操作,并保存到Log Buffer中;
- 执行
commit操作,此时MySQL会将Log Buffer中的内容刷新到磁盘中(由innodb_flush_log_at_trx_commit控制),刷新成功后才会给用户返回操作成功的提示。这样,操作记录就持久化到磁盘中了,并且由于日志是顺序写的,内容较少,所以速度是快的,不会影响体验。 - 哪怕此时系统崩溃,内存中的数据丢失,之后也可以通过Redo log恢复数据,保证了提交了的事务是不会丢失的。
Doublewrite Buffer(双写缓冲区)的作用是为了解决部分写失效问题:InnoDB 存储引擎的数据读写最小单位是页 (Page),通常是 16KB。操作系统读写磁盘的最小单位通常是 4KB 或 8KB。当 InnoDB 将一个 16KB 的数据页从内存写入磁盘时,如果操作系统层面的写操作(例如分 4 个 4KB 的块写入)在中间发生中断(比如服务器掉电、操作系统崩溃),就可能导致磁盘上的数据页只写入了一部分,形成一个不完整或损坏的页。一个被部分写入的页是无效的,而且更糟糕的是,仅仅依靠 Redo Log 无法恢复这种损坏的页。Redo Log 记录的是对数据页的逻辑物理修改(例如“将页 X 的偏移 Y 处的值从 Z 改为 W”),它假设被修改的页本身是完整的。如果页本身已经损坏,Redo Log 无法将其恢复到一个一致的、可用的状态。
具体解决方法可以查看前一篇文章:InnoDB存储引擎
2. Atomicity-原子性
原子性的定义: 一个事务中的所有操作,要么全部成功执行,要么全部不执行(回滚到事务开始前的状态)。没有中间状态。
实现原子性的原理:Undo Log(重做日志)
以下面的例子讲解Undo Log的使用
sql
begin;
update t set name='a' where id=1;
update t set age=20 where id=1;
update x set address = '重庆' where name='张三';
rollback;假设现在开启一个事务,事务ID为999,那么执行三条update语句时,会产生三条Undo Log记录,与事务999相关联,示意图如下:

当执行rollback语句时,会找到与事务999相关联的Undo Log记录,执行其中的内容,达到回滚的效果。
Undo log 记录分为两类:
- Insert Undo Log:记录
INSERT操作。 - Update Undo Log:记录
UPDATE和DELETE操作。
3. Isolation-隔离性
隔离性的定义:多个并发事务之间互不干扰,一个事务的中间状态对其他事务不可见。事务看起来就像是串行执行一样。
实现隔离性的机制:锁(Locking)+ 多版本并发控制(MVCC)
3.1 MVCC
之前我们已经详细介绍过锁了,这里主要介绍MVCC。
MVCC,Multi-Version Concurrency Control,多版本并发控制,是指维护一个数据的多个版本,使得读写操作没有冲突。MVCC的实现依赖数据库记录中的三个隐藏字段、Undo Log和ReadView。
在介绍MVCC之前,先介绍三个隐藏字段。在MySQL中,为了维护的需要,添加了三个隐藏字段:DB_TRX_ID、DB_ROLL_PTR和DB_ROW_ID。

当多个事务对同一条记录执行写操作时,会形成一条Undo Log版本链,通过DB_ROLL_PTR相关联,链表的头部是最新的记录,链表的尾部是最早的记录:

接下来介绍ReadView。先理解两个概念:
- 当前读:读取的是记录的最新版本,并且读取时还要保证其他并发事务不能修改当前记录,所以会对读取的记录加锁。如
select ... for share,select ... for update,insert,update,delete都是一种当前读; - 快照读:简单的
select(不加锁)就是快照读,读取的是记录数据的可见版本,有可能是历史数据,不加锁,是非阻塞读。
进行快照读时,就会生成一个ReadView对象,其中包含了四个关键字段:

当事务对某条数据进行快照读时,根据生成的ReadView对象,和Undo Log链中记录版本数据中的trx_id进行比较,就能判断是否可以访问该版本数据:

3.2 不同隔离级别
READ UNCOMMITTED-读未提交:既不会加锁,也不会使用MVCC。
READ COMMITTED-读已提交:使用加锁机制和MVCC机制。
在事务中,每一次执行快照读时,会生成新的
ReadView对象。例如:
在事务5中第一次执行快照读,生成一个
ReadView对象,然后根据Undo Log版本链中查找可以读的记录,找到(红框):
在事务5中再次执行快照读,又生成一个新的
ReadView对象,再根据新的ReadView去Undo Log版本链中查找可以读的记录,找到另一条记录(红框):
REPEATABLE READ-可重复读:使用加锁机制和MVCC机制。
仅在事务中第一次执行快照读时生成
ReadView对象,后续服用该ReadView对象。例如:
在事务5中执行两次快照读,第二次直接复用第一次的
ReadView,所以和第一次的结果相同:
SERIALIZABLE-串行化:使用加锁机制。
4. Consistency-一致性
一致性的定义: 事务从一个数据库的合法状态(满足所有约束,如唯一性、外键、检查约束等)转换到另一个合法状态。
实现一致性的机制:很大程度上是原子性和隔离性的结果。
原子性保证了数据不会出现“部分完成”的混乱状态。
隔离性保证了并发操作不会导致数据冲突和不一致。
参考
[1] https://catkang.github.io/2021/10/30/mysql-undo.html