1. 背景
Atomicity 原子性的定义,原子性保证了在一个事务中,多个数据库操作,要么全部成功,要么全部失败,不允许出现部分成功,部分失败的情况。
ref 维基百科:https://en.wikipedia.org/wiki/Atomicity_(database_systems)
Durability 持久性的定义是,在一个操作完成之后,数据必须是持久化的,也就意味着不会丢失。
这篇文章会围绕着 A 和 D 来展开讨论,因为 A 和 D在mysql 的实现过程中,很多技术点和思路是一样的,所以放在一起讨论。
2. 核心思路
这个图是 mysql 在修改数据的时候核心思路,里面有几个重要的概念。
- redo log:redo log 是在提交的时候用来记录事务修改的物理文件,其中记录了这次事务提交修改的内容。
- undo log:undo log 是在修改数据之前,对于老的数据进行数据的备份。
- 缓存页:缓存页是 mysql 在查询磁盘上的数据页之后,缓存在内存中的缓存。
- 脏页:脏页是在对内存中的数据进行更新之后,未同步到磁盘的缓存页,也就是缓存页和磁盘的数据页数据不一致的时候称为脏页
- bin log:bin log 是执行引擎写的数据库执行的记录,用于主备同步
- checkpoint:checkpoint 是指的在某个时间点,mysql 会把脏页刷到数据页上,checkpoint 记录了最后刷新的位置。
除了这些概念以外,还有几个重要的设计思路,write-ahead log,checksum,CopyOnWrite,DoubleWrite 等设计思路,用于解决实现过程中的问题。
3. Write-Ahead Log
WAL (write-ahead log) 的引入的核心目的是为了提升 mysql 的 mysql 写性能,核心思路是把 随机写 变成 顺序写。
如果不采用 WAL 机制的话,每一次在修改数据库的时候,都需要定位这条数据在磁盘上的位置,即会有大量的写 IO。
那如果不想产生大量 IO 的话,只能在内存修改成功之后返回成功,再批量同步到磁盘,但是这样的做法又会出现宕机内存丢失,导致数据丢失的情况。
最终 mysql 采用 WAL 机制后,在提交事务的时候,把需要变更的内容写入另外一个文件中,也就是 redo log。在之后的某一个时间点,再批量把内存中的数据刷到表空间的数据磁盘上,以提升整体的性能、吞吐量,在此期间,如果服务器宕机的话,可以通过 redo log 来恢复数据。
redo log 在逻辑上可以认为是一个无限长度的字符串,当数据库有数据变更的时候,会在redo log末端追加变更的内容,也就是顺序写。
顺序写 随机写的性能对比:
4. Redolog
4.1. 核心思路
redo log 又称为重做文件,目的是用于做mysql 宕机数据恢复的,因为mysql采用了 WAL 的写机制,在 redo log 写成功之后就会返回事务提交成功,所以再数据还没刷新到磁盘数据页的时候 服务器宕机的话,重启之后会通过 redo log 进行内存的数据重做,保证数据的原子性和持久性。
4.2. redo log 结构
redo log 在逻辑上是一个无限长度的字符串,可以一直在尾部进行追加。因为在物理上是不可能无限长度的,物理上是通过文件覆盖的形式做的,比如有4个redo log 文件 (ib_logfile_0, ib_logfile_1, ..2 , ..3),当 0 写满了会写1,1写满了写2,以此类推,当3写满了再写1。
在 redo log 中会有 check point 机制,checkpoint 表明这部分之前的 redo log 已经刷到数据盘了,所以当前写的位置到 checkpoint 的位置是空白的,是可以被重写的(具体checkpoint 的机制,在后面再详细解释)如图:
因为磁盘是块设备为了保证 IO 的效率,innoDB 的读写不是一个一个字节来读写的,是通过 块 来读写的,所以 redo log 在设计中,也是 512 个字节为一个块,一个 redo log 的文件是通过N个 512 个 block 块组成的,结合之前说的 redo log 是循环使用的,最终在物理磁盘上的体现如下:
每一个 Log Block 中的结构如下,其中包含了头部12个字节,结尾4个 checksum字节,所以当中做输出存储是 496 字节。
如果一条数据在一个 block 中保存不下,可能会跨多个 block来保存。
数据中 LSN (Log Sequence Number) 是一个64位无符号整数,表示Redo Log系统中的时间点,也是事务写入Redo Log的字节总量,LSN 最大的作用就是在重启的时候,mysql 来判断需要重新执行多少条 LSN 的语句,以此来恢复 buffer pool 中脏页的数据,以保证数据的一致性,具体如何恢复的在 checkpoint 中解释。
因为 LSN 是通过写入的字节总数来作为编码的,所以 LSN 可以计算出来他所属的 Block,因为 LSN 是单调递增,所以给定一个 redo log,也可以计算出第一个 LSN 是什么。
比如 (200,289,378,478,30,46,58,69,129) 第1条日志是30,最后1条日志是478,30以前的已经被覆盖。
4.3. redo log 和事务的关系-commit
假设应用提交了如下 sql 的修改
start transaction
update from A set value = ? where id = ?;
delete from A set where id = ?;
insert into B values(?);
commit;
在应用层面,我们感知到的是一个逻辑事务,在 innoDB 内部,逻辑事务会拆分成多个物理事务,称为 mini-transaction (mtr),一个 mtr 对应一个 page 的原子操作,一个sql 可能会对应多个页(因为一个sql 可能操作的数据一个页放不下,或者索引比较多,在不同的页,一个页又对应一组 redo log,结构如下:
一个逻辑事务在 redo log 中的存储并不一定是连续的,但是一个原子性的 mtr 事务在 redo log 的存储一定是连续的,假设有2个逻辑事务在并行,每个逻辑事务都会产生多个 mtr 物理事务,在一个事务下的多个 mtr 通过链表来关联,都会关联到 xts id 上,xts id 代表事务 id,一个单调递增的 id,最后在 redo log 中的记录可能是这样的。
那在 redo log 写入完成之后(如果不考虑bin log 一致性的事情),事务的提交就已经完成了,总结一下这个过程中做了多少事情。
- 内存中 buffer pool 的数据已经变化,修改为最新的数据
- 内存中 log pool 的 redo log 写入了这次变化的记录,记录到哪一页修改了什么
- 磁盘上存储了 log pool 的 redo log 的变更。
4.4. 问题
4.4.1. 一次 redo log 的写入是 512 个字节,那如果写入失败了,怎么判断数据是否完整?
在 redo log 刷盘的时候,512 个字节为一个 block,那当刷盘的时候,如果出现了异常,或者服务器宕机了,那是刷了多少数据呢?0k 还是 200k 还是 512k,是不得而知的,一个未知数,所以在 redo log 的数据结构中,结尾的 4个字节 log block tailer 中存放的是 checksum 的结果。
checksum 是通过某种算法,对于数据进行编码后的一个值。在宕机恢复的时候,读取 redo log,如果checksum 是匹配的则证明这个数据是完整的,否则是损坏的,对于损坏的 redo log 被认为这个事务没有提交,不需要恢复其中的数据,则可以丢弃这个 block,做截断处理。
4.4.2. redo log 在提交的时候刷盘,因为逻辑事务可能是不连续的,可能会把其他未提交的事务的 redo log 也刷到盘上,有什么影响?
这个和 checkpoint 机制有关,在数据恢复的时候,innoDB 会基于 checkpoint 的数据,识别出哪些 redolog 是已经提交的,哪些 redolog 是没有提交,服务器宕机的,也就是意味着需要回滚的,如果是需要回滚的话,会基于 undolog 生成逆向sql进行数据回滚,从而达到数据的原子性和一致性。
5. 宕机恢复 checkPoint
5.1. 核心思路
之前分析过,redo log 中会包含未提交的事务,在宕机恢复时,innoDB 会把 redo log 中的操作全部捞出来,进行重放(这也是为什么 redo log 叫 redo log),重放的数据包括了提交的和未提交的,从而让数据 库“原封不动”地回到宕机之前的状态,这叫Repeating History,在重放之后在判断出哪些是未提交的事务,在进行 rollback。
5.2. 问题
- 如何判断要重放哪些 redolog?
- 如何判断出来 redo log 中的数据是否要回滚?
5.3. checkpoint
checkpoint 是一个特别重要的点,他核心解决了上面2件事情,一个是记录了表数据磁盘上已经刷到 redo log 的什么位置,第二个在 checkpoint 的数据中保存了当前数据库活跃的事务(也就是未提交的事务)和脏页。
5.3.1. 刷盘
我们前面分析过了,innoDB 在进行写操作的时候内存和redo log写完就返回成功,这个时候,内存中的缓存页和磁盘的数据是不一致的,这也称之为脏页。脏页在达到某个阈值的时候需要刷到磁盘上去。
那刷盘如果是想一次性把所有脏页都刷到磁盘上去是几乎不可能的,因为有开启的时候正在不断的更新缓存页。
除非把系统阻塞住,进行刷盘,并且不接受新的请求,redolog 也不增长,这种称之为 Sharp Checkpoint。
而另一种则是 fuzzy checkpoint,选择一定量的脏页,刷到磁盘上去,并且把这个事件记录在 redo log,记录完成后,redo log checkpoint 之前的数据就是可以被覆盖的,如图:
5.3.2. 刷盘的时机和作用
刷盘的时机有很多,比如
- redo log 写满了,无法在写入了,必须要刷一部分脏页到磁盘,redo log 才能继续往前推进。
- buffer pool 满了,内存满了也是一个道理,需要加载新的数据进到内存,但是内存已经满了,通过 LRU 算法淘汰一部分缓存页,在淘汰之前也要刷盘。
- mysql 还有一些启动参数可以来决定什么时候刷盘
刷盘的作用
其实如果 redo log 真的是无限大的文件的话,可以保存几年,那脏页其实可以一直不刷盘,那带来的问题就是重启的时候会变慢,因为需要把过去几年的 redo log 全部重放一遍才能做到数据的一致性。
所以刷盘的核心的作用就是让mysql 的启动变得更快,释放内存,释放 redolog
5.3.3. 问题
5.3.3.1. 数据写入丢失
刷盘的时候,innoDB 不是一个一个字节的读写,因为磁盘是块单元,每次读写都是以块的维度进行读写,一次读写一个页 = 16K,上面所说的 mini transaction 也是一个页的维度,那为什么说一个页是一个原子操作呢,怎么保证一次16k的写入是完整的?如果写了一半失败了,可能会导致这页的数据损坏,并且是无法通过 redo log 来恢复的,因为 redo log 只记录了这次变更的内容,如果页损坏的话,redo 无法无法识别出是这一页哪里出了问题。
解决方案:mysql 是通过 double write 来保证的一个页写入的原子性的,在需要写入页的时候会写入一个临时区域,写入成功后再拷贝到目标磁盘区域。
- 如果写临时区域失败了,那原始的数据不会损坏,通过 redo log进行重放即可。
- 如果是拷贝失败了,那可以通过临时区域的数据重新拷贝。
5.4. 宕机恢复
5.4.1. 宕机恢复的核心逻辑
宕机恢复的核心思路是通过 ARIES(Algorithm for Recovery and Isolation Exploiting Semantics) Recovery 算法来实现的。核心工作流程分为以下3部
- 分析阶段:分析哪些数据是脏页,哪些事务是未提交的
- 执行 redo:进行重放
- 执行 undo:对于未提交的事务,进行回滚
上图中有4个事务,check point 的时候记录了活跃事务是 T2 和 T3,之后开启了事务 T4,机器宕机了,此时 T4 和 T2 的事务还没完成,所以最终我们期望得到的应该是,T1 和 T3 的事务是提交的,T2 和 T4 的事务应该回滚。
这个时候在 redo 上的数据记录是如下
5.4.2. 问题
在这个里面需要解决的问题
- 未提交的事务:一个逻辑事务在 redo log 上记录可能是不连续的,所以再其他事务写 redo log 磁盘的时候,会带上未提交的事务,这部分事务是需要回滚的,这个怎么判断出来?
- 脏页:fuzzy checkpoint 在刷盘的时候,只会刷一部分的数据到数据盘上,也就是说内存中还有脏页,这部分脏页是需要执行 redo log 来保证一致性的,比如上图的事务3,在 checkpoint 之后 宕机之前已经提交了,但是是在 checkpoint 之前,所以 T3 的数据是没有刷到磁盘的,怎么保证一致性呢?
5.3.1. 分析阶段
innoDB在运行过程中,会在内存中记录2个表,一个是脏页的数据表,一个是当前活跃的事务的列表。
活跃事务表:
字段名 |
解释 |
tx_id |
事务编号 |
lastLSN |
当前事务修改的最后一个 LSN |
脏页表:
字段名 |
解释 |
pageNo |
脏页的页号 |
recoveryLsn |
这个脏页最早修改他的 LSN,有多个修改取最早的一个 |
在每一次 checkpoint 的时候,会把这两个表的数据,做一个快照,写入 redo log 中。
这两个表用来解决上面的2个问题
5.3.1.1. 未提交的事务
还是以上面图的为例子,innoDB 在读取 redo 之后,会找到最近的一次 checkpoint,其中获得未提交的事务为 {T2, T3},然后从 checkpoint 一直读到 redo log 末尾,在读取到 T3 Commit 标记的时候,把 T3 从集合中移除,得到是 {T2},在读到 T4 Start Transaction 的时候,把 T4 加入集合中,所以结果是 {T2, T4}。
所以得出 T2 和 T4 是需要回滚的事务。
5.3.1.2. 脏页
和上面一样,innoDB 拿到最近的一次 checkpoint,获得其中的脏页为 Page 1 和 Page 2,以此读到文件末尾,如果有数据变更,但是不在脏页集合内的,加入到脏页集合,最终可能是 {P1, P2, P3},通过脏页获得到最早开始的 LSN,进行重放。
5.3.2. 重放阶段
重放阶段是开始做 redo log,以上图为例, T3 事务就是需要重放的,我们前面提过一个逻辑事务可能包含多个物理事务,所以在 checkpoint 的时候,可能这个逻辑事务中一个物理已经写入了 redo log,另一个没有写入 redo log,并且这个数据页是脏页,没有刷到数据盘上,之后服务器宕机了。那这个时候,从磁盘加载出来的数据是脏页,就需要重做这个脏页所有的 redo(可能会在 checkpoint 之前),挨个执行 redo。
上面的例子,基于checkpoint 拿到2个脏页集合,page1和page2,以此往后读到文件末尾,发现开启了事务4,对应page3,加入到脏页集合,就是 {page1, page2, page3}。
取这3个page最早的 LSN,page1 page2 的 first LSN 是在 checkpoint 中记录的,分别为 200 和 320,而事务4则是后面加入的,是 500,最早的 LSN 为 200.
redo 则是从 200 开始执行 redo 回放,一直执行到文件末尾,那这之中可能有写脏页已经刷到磁盘了,但是我们通过这个是无法得知的,这个没有问题,因为 redo log 的重放可以做到幂等,多次重放获得结果是一致的。
5.3.3. undo 阶段
在阶段1分析阶段的时候,已经确定了未提交的事务是 T2 和 T4,也就是这两个事务是需要回滚的。
我们在 redo log 结构 中也提到过,同一个逻辑事务的多个物理事务,是通过链表关联,后一个物理事务中有 prevLSN,来表达这个逻辑事务下,当前物理事务的前面一个物理事务。
所以通过 checkpoint 的数据,我们得知 T2 的最后一次修改的 LSN 是 200,T4的最后一次修改是 520,因为 T2 最后一次修改之前还有一次 redo log 的修改,是 120 的 LSN,所以需要 undo 的有3个,分别是 120 200 520.
然后基于 undo log 生成 Compensation Log Record(CLR) 回滚语句,追加在Redo Log尾部。所以对于Redo Log来 说,其实不存在所谓的“回滚”,全部都是Commit,日志也只会追加,不会执行修改或者阶段之类的操作。
但是想要生成逆向的 sql 语句,就需要有原始的数据,这个就是 undo log
6. undo log
6.1. undo log 概念 和 作用
undo log 其实是数据的备份,他和 redo log 不一样,redo log 类似于执行日志,undo log 却是真正的数据。
undo log 的作用主要有2个,一个是用来做事务的回滚,第二个是用来做多版本控制 MVCC,这也是 innoDB 如何保证隔离性的手段,见:https://yuque.antfin.com/yixin.cxw/erxs7e/toko6h
6.2. 生成 undo log
举个和 redo log 一样的例子
start transaction
update from A set value = ? where id = ?;
delete from A set where id = ?;
insert into B values(?);
commit;
如果把 undo log 加进去之后会变成这样
start transaction
写 undolog1 数据备份
update from A set value = ? where id = ?;
写 redo log1
写 undolog2 数据备份
delete from A set where id = ?;
写redo log2
写 undolog3 数据备份
insert into B values(?);
写 redo log3
commit;
这里的写是把 redo log 和 undo log 都写进内存里面,只有在最后 commit 的时候才把 redo log 写入磁盘中,至于 undo log,之后通过异步刷盘的方式在刷入磁盘,如果因为宕机导致 undo log 丢失,可以通过当前数据 + redo log 来恢复 undo log。
7. binlog
7.1. 概念
bin log 和 redo log 看似差不多,但是区别蛮大的,redo log 和 undo log 是 innoDB 存储引擎内部的机制,而 bin log 是 mysql 层面写的日志,如果换一个存储引擎,不用 innoDB 了,可能没有 redo log 和 undo log,但是还是会有 bin log.
7.2. 作用
bin log 主流的作用主要是做 mysql 主备之间的同步,或者把某个服务器伪装成 mysql 的 slave 做数据同步。
7.3. binlog 一致性的问题
7.3.1. 问题
在写入一条数据的时候,innoDB 需要写入 redo log, undo log, mysql 又要写入 bin log,也会有一致性的问题,
- 如果先写 bing log 后写 redolog,binlog 成功,redo log 失败,服务器宕机的话,数据就丢失了,但是 slave 获取到了 binlog,会更新数据,导致主从不一致
- 如果先写 redo log 后写 bin log,也会有类似的问题
7.3.2. 解决方案
mysql 是通过 2PC 的思路来解决的
阶段1:
- innoDB 收到需要提交事务的请求,innoDB 先进行 prepare 操作,确保 redo log 的变更记录落盘,但是这个时候是没有 COMMIT 记录的,只有变更记录。
- mysql 收到事务可以提交的响应之后,把 binlog 写入内存。
阶段2:
- mysql 把 binlog 刷入磁盘,请求 innoDB commit 操作
- innoDB 收到 commit 操作后,把内存中的 redo log 的状态修改为 COMMIT,写入 redo log COMMIT 标记
7.3.3. 异常推演
- 没有 redo log,在阶段1 宕机
阶段1失败并且没有 redo log 刷过盘表示磁盘上没有任何数据,服务器宕机,内存中的数据全部消失,没有 redo log 没有 bin log,服务器重启之后数据恢复到以前的状态,和回滚效果一样。
- 有部分 redo log 已经刷盘了,在阶段1的宕机
有部分的 redo log 已经刷盘了,但是没有 bin log,依赖 undo log 生成 CLR 进行回滚处理。
- 在阶段2中第一步执行了一半失败了,binlog 没写全,宕机了
bin log 没有写全,说明这部分 binglog 是损坏的,对于后续损坏的做阶段处理,事务一样做回滚处理
- 在阶段2种第一步执行成功了,redo log 没有写,宕机了
此时遍历 Binlog,Binlog 中存在、InnoDB中不存在的事务,以 binlog 为准,发起commit 操作,补上 redo log 中的 COMMIT 标。
8. 参考文献
https://www.cnblogs.com/f-ck-need-u/p/9010872.html
https://www.cnblogs.com/ZhuChangwu/p/14096575.html
https://www.cnblogs.com/kismetv/p/10331633.html
https://www.lixueduan.com/post/mysql/07-binlog-redolog-undolog/
https://en.wikipedia.org/wiki/ACID#Consistency_(Correctness)
https://en.wikipedia.org/wiki/Atomicity_(database_systems)
https://en.wikipedia.org/wiki/Consistency_(database_systems)
https://cloud.tencent.com/developer/article/1801920
https://www.cnblogs.com/Howinfun/p/15359777.html
https://blog.csdn.net/javaanddonet/article/details/112596210
http://fishleap.top/pages/c62f50/#%E7%9F%A5%E8%AF%86%E7%82%B9
https://juejin.cn/post/6976060698757595150
http://mysql.taobao.org/monthly/2015/04/01/
https://juejin.cn/post/6895265596985114638
https://www.cnblogs.com/Howinfun/p/15359777.html
https://jimmy2angel.github.io/2019/05/07/InnoDB-undo-log/
https://www.leviathan.vip/2019/02/14/InnoDB%E7%9A%84%E4%BA%8B%E5%8A%A1%E5%88%86%E6%9E%90-Undo-Log/