在我们要深入了解MySQL时,一定会听说大名鼎鼎的三大日志系统 Redo Log(重做日志)、Bin Log(备份日志)和Undo Log(回滚日志)
在整个数据库架构中,日志系统记录了数据库的操作和状态变化。数据的恢复、事务的隔离等级实现、事务的原子性隔离性等等,都要通过日志系统实现
本篇文章会探讨日志系统的作用以及原理,分析如何实现某些功能
不需要有太多的知识储备,我会尽量讲的通俗易懂
MySQL数据库模型
在研究原理之前,先来了解数据库的基本架构
大体来说,MySQL 可以分为 Server 层和存储引擎层这两部分
Server 层包括连接器、查询缓存、分析器、优化器、执行器等,涵盖 MySQL 的大多数核心服务功能,以及所有的内置函数(如日期、时间、数学和加密函数等),所有跨存储引擎的功能(不依赖特定存储引擎的功能)都在这一层实现,比如存储过程、触发器、视图等
而存储引擎层负责数据的存储和提取。其架构模式是插件式的,支持 InnoDB、MyISAM、Memory 等多个存储引擎。现在最常用的存储引擎是 InnoDB,它从 MySQL 5.5.5 版本开始成为了默认存储引擎,也就是说,在执行create table语句建表的时候,如果不手动指明引擎类型,默认使用的就是InnoDB
从图中也可以看出来:
不同的存储引擎共用一个 Server 层
现在不需要了解每一个部件用来干嘛的,也不需要了解各引擎支持的功能。只需要知道数据库大概分为两层就可以了
不同的引擎对表数据存取的方式不同、支持的功能也不同,下面的介绍都默认使用 InnoDB 引擎
Redo Log(重做日志)
在介绍 Redo Log 之前,先来看一个小示例:
在《孔乙己》这篇文章,酒店掌柜有一个粉板,专门用来记录客人的赊账记录。如果赊账的人不多,那么他可以把顾客名和账目写在板上。但如果赊账的人多了,粉板总会有记不下的时候,这个时候掌柜一定还有一个专门记录赊账的账本。
如果有人要赊账或者还账的话,掌柜一般有两种做法:
- 一种做法是直接把账本翻出来,把这次赊的账加上去或者扣除掉;
- 另一种做法是先在粉板上记下这次的账,等打烊以后再把账本翻出来核算。
在生意红火柜台很忙时,掌柜一定会选择后者,因为前者操作实在是太麻烦了。首先,你得找到这个人的赊账总额那条记录。你想想,密密麻麻几十页,掌柜要找到那个名字,可能还得带上老花镜慢慢找,找到之后再拿出算盘计算,最后再将结果写回到账本上。
这整个过程想想都麻烦。相比之下,还是先在粉板上记一下方便。如果掌柜没有粉板的帮助,每次记账都得翻账本,效率是不是低得让人难以忍受?
(示例来源于其他文章,结尾有贴出处)
在MySQL中也有这个问题,如果每一次更新操作都要写进磁盘,那么磁盘也要找到对应的那一条记录,然后再更新。
磁盘存储的基本单元是页,就算一次修改只改变了一页中的几个字节,修改后也需要刷新整页的数据。并且如果一次事务同时修改了多页中的数据,页之间又不连续,会因为磁盘的随机IO(只需要知道访问数据不是按照连续地址)浪费不少性能
MySQL的设计者为了提升效率,用了类似于上面粉板的思路,这其实就是MySQL中的WAL技术
WAL全称为Write-Ahead Logging,即预写日志系统,大概就是MySQL 在执行写操作的时候不是直接去磁盘上更新,而是先记录在日志里
写入日志是顺序写操作,每次写操作只需要在末尾加上这次操作就可以,比磁盘随机访问效率要高很多。日志完成后,系统只需要在空闲时间根据日志往磁盘上更新。这样就把随机写转换为了顺序写,大大提高了数据库的性能
所以,对于一次写操作,存储引擎会先把这个操作写在 redo log(粉板)中,等到空闲的时候再把操作记录更新到磁盘中
redo log 是有一定大小的,InnoDB 中的 redo log 配置为一组4个文件,每个大小是1GB,一共是4GB。在写入日志时,会以顺序循环的方式写入这4个文件,写满时则回溯到第一个文件,进行覆盖写
write pos 记录当前写入位置,随着写入不断后移,写到最后一个文件末尾(3号文件)后会从0号文件的开头继续写,如此不断循环
check point 是当前要擦除的位置,负责把已经向磁盘中更新的记录擦除,也是不断后移循环
write pos 到 check point 之间的是待写入磁盘的记录。如果 write pos 追上了check point,说明文件被写满了。必须先擦掉一些记录,即先往磁盘上更新之后,才能继续往下写
有了 redo log,执行写操作效率大大增加,并且保证了即使数据库发生异常重启,之前提交的记录也不会丢失(这个能力称为 crash-safe)
要理解 crash-safe 这个概念,可以想想前面赊账记录的例子。只要赊账记录记在了粉板上或写在了账本上,之后即使掌柜忘记了,比如突然停业几天,恢复生意后依然可以通过账本和粉板上的数据明确赊账账目
Bin Log(备份日志)
上面提到的redo log是InnoDB引擎特有的日志,属于存储引擎这一层。
而bin log存在与Server层
在最开始,MySQL 自带的引擎是MyISAM,MyISAM 只有 bin log 日志用于归档备份,而没有 crash safe 能力(下面会解释为什么),而 InnoDB 是另一个公司以插件的形式引入 MySQL 的,既然依靠 bin log 没有 crash safe 能力,InnoDB 就使用了另一套日志系统,也就是 redo log 来实现 crash safe 能力
那既然 bin log 没有 crash safe 能力,为什么还要用它呢?
因为 bin log 主要作用是用来归档备份,它记录的是每次操作的原始操作逻辑,比如你要更新一条数据,它会记录 “给 ID=2 这一行的c字段加1” 类似这样的一条记录,而 redo log 记录的是真正要对磁盘页的修改
所以 bin log 是逻辑日志、redo log 是物理日志
bin log 会根据配置,去记录规定的每一条记录,并且 bin log 是追加写入的,它不会像 redo log 那样循环写入,当 bin log 文件写入一定大小后会切换到下一个,不会覆盖以前的日志,所以能实现把数据库恢复到备份期间的任意一刻
现在来解释为什么 bin log 日志不能实现 crash safe,因为记录的是操作逻辑,并且写入的方式是追加写入,也就是说 bin log 会记录全量日志,不能通过这个日志看出来哪些逻辑已经真正执行了。假如现在数据库突然异常中断,从 bin log 日志的内容中是判断不出哪些数据是要恢复的,因为没有一个标志去判断哪些操作已经执行
而 redo log 中记录的是真正已经执行的操作,只需要把日志中的数据挪到磁盘上,挪一块就会把这一块的数据擦除掉,所以 redo log 中保存的一直都是待转入磁盘的数据,就算中途数据库异常中断了,待转入磁盘的数据也不会丢
来看一个应用场景:
某天你发现中午十二点有一次误删表,现在要找回数据,你就要使用 bin log 日志,从备份的时间点开始,将备份的 bin log 中的记录取出来,一条一条执行,一直执行到误删表的那个时刻,这样就完成了数据的恢复
Undo Log(回滚日志)
MySQL 最初默认使用的MyISAM引擎不支持事务,undo log 是 InnoDB 引擎特有的日志,undo log 日志用于撤销回退,事务的原子性就依靠 undo log 日志实现
事务的原子性就是指事务中的所有操作要么全部成功提交,要么全部失败回滚
undo log 也是逻辑日志,每执行一次语句时,undo log 就会记录一次反向操作用于回滚
举个例子,一个值从1被按顺序改成了2、3、4 最后把这条记录删除,那么undo log 中就会按顺序记录 ‘把2改成1’、‘把3改成2’、‘把4改成3’、‘插入一条值为4的记录’ 这种反向操作。
所以当执行事务失败,要进行回滚操作时,就可以根据 undo log 的内容回滚到事务执行之前
undo log 还有很多应用,如实现事务的隔离级别和 MVCC(多版本并发控制)
为了更好的理解 undo log 的原理,要稍微说一下事务的隔离级别
以事务的 可重复读 隔离等级说明,可重复读的隔离等级是指,一个事务执行过程中看到的数据总是和启动时看到的一致。
在可重复读隔离级别下,事务执行期间所做的任何修改不会被其他事务看到,同理其他事务做的修改和当前事务也没有关系,即使其他事务已经提交(实际上有时候能看到,比如使用当前读(current read)就能看到其他已经提交事务的修改,但这里只是为了了解 undo log 的原理,简单理解为不能看到就行)
当一个事务开始时,MySQL会创建一个事务视图,这个视图包含了事务开始时数据块中所有数据的一个快照,在事务执行期间,所有数据都来自于这个快照。各个事务有各自的视图,互不干扰
为什么能实现事务视图之间互不干扰呢?因为同一条数据是可以存在多个版本的,这就是数据库的多版本并发控制(MVCC)
看一个例子,一个值 v 从1被按顺序改为2、3、4,假如现在不同时刻存在三个事务。在值为1时开启事务A,值为3时开启事务B,在A、B各自的视图中,这个值分别为1、2
上面也说了,每执行一次语句,undo log 就会记录一条反向操作的语句,现在undo log 已经记录下了 v 的变化
实际上,v 经过一系列修改后,现在实际值为4,即它在表中的数据已经被修改为了4。而事务A、B中读取的值是 v ”之前版本的数据“,是通过 undo log中的记录,用当前实际值 4 进行倒推得到的值
也就是说,事务A、B中读到的数据,是数值 v 之前的版本,在创建事务视图时,会给数值加版本号,事务中每次取这个值时判断当前的版本号,如果版本号高了,就根据 undo log 中的记录进行回退,回退到当前版本号为止
这样就实现了高效的并发控制和事务隔离,并且保证了事务的原子性
undo log 中的记录不可能一直保存,当系统判断没有事务再需要回滚日志中的这些记录时,就会把这些记录删除
总结
这篇文章重点介绍了物理日志 Redo Log 和逻辑日志 Bin Log和 Undo Log,Bin Log 和 Undo Log 是 InnoDB 引擎独有的,Redo Log 存在于 Server 层,被每一个存储引擎共用
Redo Log 实现了 crash-safe,保证了即使数据库异常退出,记录也不会丢失
Bin Log 用于归档备份,实现了把数据恢复到备份期间任意一刻
Undo Log 用于回滚,实现了事务的原子性、事务的隔离等级以及多版本并发控制(MVCC)
Redo Log 示例参考:
https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/MySQL%e5%ae%9e%e6%88%9845%e8%ae%b2/02%20%20%e6%97%a5%e5%bf%97%e7%b3%bb%e7%bb%9f%ef%bc%9a%e4%b8%80%e6%9d%a1SQL%e6%9b%b4%e6%96%b0%e8%af%ad%e5%8f%a5%e6%98%af%e5%a6%82%e4%bd%95%e6%89%a7%e8%a1%8c%e7%9a%84%ef%bc%9f.md