我正在参加「掘金·启航计划」
日志在程序中是什么样的存在?其实感觉就像是人类的历史书,记录着人类世界发生的大大小小事件,然后以史为鉴解决未来的问题。MySQL中的日志也是起到这样一个作用:
- 比如数据在内存中已经修改但没有来得及写入磁盘永久保存系统就崩溃了,这时就需要日志来恢复数据;
- 又比如我们在事务中已经修改了数据,但是突然出现错误要撤销操作,这时也需要记录的日志帮我们一步一步还原数据。
数据库中的数据一般来说对现实世界是非常重要的,一旦丢失就会造成不可挽回的损失,所以日志的存在就非常重要,我们在学习MySQL时必须要掌握日志,尽量避免以后工作中可能出现的失误。
日志分类
现实需求决定着技术的实现,关于日志的分类我们可以现实需求着手的记忆,想一想不同的日志解决的是什么问题。可以看一下我上面提到的日志的两个作用,分别对应着下面两种日志:
- redo log(重做日志) :是 Innodb 存储引擎层生成的日志,实现了事务中的持久性,主要用于掉电等故障恢复;
- undo log(回滚日志) :是 Innodb 存储引擎层生成的日志,实现了事务中的原子性,主要用于事务回滚和 MVCC。
第三种则是解决主从数据库备份问题的binlog (归档日志) ,它是 Server 层生成的日志,主要用于数据备份和主从复制。
redo log
buffer bool 缓冲池
在介绍redo日志前,我们需要知道buffer pool是什么。对于使用 InnoDB 作为存储引擎的表来说,不管是用于存储用户数据的索引(包括聚 簇索引和二级索引),还是各种系统数据,都是以页的形式存放在表空间中的,而所谓的表空间只不过是 InnoDB 对文件系统上一个或几个实际文件的抽象,也就是说我们的数据说到底还是存储在磁盘上的。这里还会出现老生常谈的问题:CPU速度与磁盘速度不匹配。所以 InnoDB 存储引擎在处理客户端的请求时,当需要访问某个页的数据时,就会把完整的页的数据全部加载到内存中。在进行完读写访问之后并不着急把该页对应的内存空间释放掉,而是将其缓存起来,这样将来有请求再次访问该页面时,就可以省去磁盘 IO 的开销了。
到这我们也就清楚了buffer pool就是在 MySQL 服务器启动的时候就向操作系统申请了一片连续的内存,其目的就是为了缓存磁盘中的页。
什么是redo log?
前面我们已经知道数据操作都是在buffer pool中进行了,这里问题就来了,假设在一个事务中我们对数据进行修改,之后突然发生了某个故障,导致内存中的数据都失效了,那么这个已经提交了的事务对数据库中所做的更改也就跟着丢失了,这是我们所不能忍受的。
解决方法:
为了防止断电或者故障导致数据丢失的问题,MySQL引入了redo log,记录了某个数据页做了什么修改,比如某个事务将系统表空间中的第100 号页面中偏移量为1000处的那个字节的值 1 改成 2,每当执行一个事务就会产生这样的一条或者多条物理日志。
- 在事务提交时,只要先将 redo log 持久化到磁盘即可,可以不需要等到将缓存在 Buffer Pool 里的脏页数据持久化到磁盘。
- 当系统崩溃时,虽然脏页数据没有持久化,但是 redo log 已经持久化,接着 MySQL 重启后,可以根据 redo log 的内容,将所有数据恢复到最新的状态。
当然这里也有一个问题:为什么不在事务提交完成之前把该事务所修改的所有页面都刷新到磁盘?
- 第一,刷新一个完整的数据页太浪费了,有时候我们仅仅修改了某个页面中的一个字节,但是在 InnoDB 中是以页为单位来进行磁盘IO的,也就是说我们在该事务提交时不得不将一个完整的页面从内存中刷新到磁盘,这样做显然会极大的浪费时间和空间。
- 第二,磁盘IO相对CPU速度实在是太慢了,而且一个事务可能包含很多语句,即使是一条语句也可能修改许多页面,大部分情况下这些页面并不相邻,这就意味着在将某个事务修改的Buffer Pool 中的页面刷新到磁盘时,需要进行很多的随机IO,这简直就是雪上加霜,因为随机IO比顺序IO更慢,无疑使得MySQL的数据处理效率大大降低。
redo log的持久化
无疑redo log也是需要写入磁盘的,这里我们需要清楚两个问题:
- 第一,redo log写入磁盘与页面写入磁盘有什么区别?
- 第二,redo log什么时候写入磁盘?
redo log写入磁盘与页面写入磁盘有什么区别?
写入 redo log 的方式使用了追加操作,所以磁盘操作是顺序写,而写入数据需要先找到写入位置,然后才写到磁盘,所以磁盘操作是随机写。
磁盘的「顺序写 」比「随机写」 高效的多,因此 redo log 写入磁盘的开销更小。
另一方面,redo log在写入时是循环写,边写边擦除日志的,只记录未被刷入磁盘的数据的物理日志,已经刷入磁盘的数据都会从 redo log 文件里擦除。
图中:
- write pos ~ checkpoint 之间的部分,用来记录新的更新操作;
- check point ~ write pos 之间的部分,待落盘的脏数据页记录;
redo log什么时候写入磁盘? 实际上,执行事务的过程中,产生的 redo log 并不是直接写入磁盘的,因为这样会产生大量的磁盘IO,即使redo log采用顺序写,耗费的时间也是很多的。
所以redo log 也有自己的缓存redo log buffer,每当产生一条 redo log 时,会先写入到 redo log buffer,那么什么时候把redo log buffer写入磁盘呢?
- MySQL 正常关闭时;
- 当 redo log buffer 中记录的写入量大于 redo log buffer 内存空间的一半时,会触发落盘;
- InnoDB 的后台线程每隔 1 秒,将 redo log buffer 持久化到磁盘。
- 每次事务提交时都将缓存在 redo log buffer 里的 redo log 直接持久化到磁盘
undo log
undo log解决的是程序“后悔”了怎么办,这个“后悔”主要指的是下面两种情况:
- 事务执行过程中可能遇到各种错误,比如服务器本身的错误,操作系统错误,甚至是突然断电导致的错误。
- 程序员可以在事务执行过程中手动输入 ROLLBACK 语句结束当前的事务的执行。
这两种情况都会导致事务执行到一半就结束,但是事务执行过程中可能已经修改了很多东西,为了保证事务的原子性,我们需要把东西改回原先的样子,这个过程就称之为回滚。
什么是undo log?
为了实现回滚操作,MySQL引入了undo log,undo log 是一种用于撤销回退的日志。在事务没提交之前,MySQL 会先记录更新前的数据到 undo log 日志文件里面,当事务回滚时,可以利用 undo log 来进行回滚。
每当 InnoDB 引擎对一条记录进行操作(修改、删除、新增)时,要把回滚时需要的信息都记录到 undo log 里,比如:
- 在插入一条记录时,要把这条记录的主键值记下来,这样之后回滚时只需要把这个主键值对应的记录删掉就好了;
- 在删除一条记录时,要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入到表中就好了;
- 在更新一条记录时,要把被更新的列的旧值记下来,这样之后回滚时再把这些列更新为旧值就好了。
undo log的作用
第一,实现事务回滚,保障事务的原子性。事务处理过程中,如果出现了错误或者用户执 行了 ROLLBACK 语句,MySQL 可以利用 undo log 中的历史数据将数据恢复到事务开始之前的状态。
第二,MySQL中MVCC多版本控制实现。
一条记录的每一次更新操作产生的 undo log 格式都有一个 roll_pointer 指针和一个 trx_id 事务id:
- 通过 trx_id 可以知道该记录是被哪个事务修改的;
- 通过 roll_pointer 指针可以将这些 undo log 串成一个链表,这个链表就被称为版本链
MVCC 是通过 ReadView + undo log 实现的。undo log 为每条记录保存多份历史数据,MySQL 在执行快照读(普通 select 语句)的时候,会根据事务的 Read View 里的信息,顺着 undo log 的版本链找到满足其可见性的记录。
bin log
bin log是server层生成的日志文件,记录的是在某个数据页做了什么修改(和redo log差不多),其主要用于数据备份和主从复制。
文件格式
binlog 有 3 种格式类型,分别是 STATEMENT(默认格式)、ROW、 MIXED
- STATEMENT:每一条修改数据的 SQL 都会被记录到 binlog 中(相当于记录了逻辑操作,所以针对这种格式,binlog 可以称为逻辑日志),但 STATEMENT 有动态函数的问题,比如你用了 uuid 或者 now 这些函数,在主库上执行的结果和在从库执行的结果出现差异,这种随时在变的函数会导致复制的数据不一致。
- ROW:记录行数据最终被修改成什么样了(这种格式的日志,就不能称为逻辑日志了),不会出现 STATEMENT 下动态函数的问题。但 ROW 的缺点是每行数据的变化结果都会被记录,比如执行批量 update 语句,更新多少行数据就会产生多少条记录,使 binlog 文件过大,而在 STATEMENT 格式下只会记录一个 update 语句而已。
- MIXED:包含了 STATEMENT 和 ROW 模式,它会根据不同的情况自动使用 ROW 模式和 STATEMENT 模式
写入磁盘的方式
binlog 是追加写,写满一个文件,就创建一个新的文件继续写,不会覆盖以前的日志,保存的是全量的日志。
这也决定了当整个数据库的数据都丢失后,我们可以采用binlog进行恢复,而不是采用 redo log。