MySQL的日志体系

1. Redo Log

熟悉MySQL InnoDB引擎的人都知道,InnoDB有一个最重要的概念就是缓冲池,这是在内存中分配的一个区域,InnoDB会将数据首先缓存在此,请求首先去命中缓冲池,无法命中缓冲池的才会在磁盘上进行检索,被检索到的数据还是会缓存在缓冲池中。

但是缓冲池依赖的内存是一种易失性的存储介质,掉电以后所有的数据都会被抹掉,为了数据的持久性,任何在缓冲池中做出的变更操作,都要持久化到磁盘上,只有这样,数据库才能实现持久性,用户也才能放心的将数据放在数据库上。

现在来看看这样一条SQL语句是如何执行的:

update table1 set table1.col1 = 1 where table1.col2 = 2;

用一个比较直观的流程图表示其过程如下:

image.png
image.png

我这里选择性的忽略了一步,就是有一个线程,采用异步的方式,慢慢的将脏数据页从内存中刷入磁盘的数据文件中。这种commit之后首先写redo,然后异步写数据文件的方式,叫做WAL,即预写日志方式。

有了WAL方式,我们的数据只要commit成功就绝对不会丢失了。InnoDB的REDO LOG有两个可以控制的参数,规定了日志文件最大的大小和一共有多少日志文件。

但是不管有多少个文件,都可以看做是一个文件,redo文件是循环利用的,即文件写满了,就会回收空间。在很多资料上,Redo Log文件都会被画成一个环,实际上也确实如此。

我们知道,Redo的存在保证了持久性,所谓持久性,一般都可以理解为只要提交的事务就一定会持久化。那么Redo是如何保证持久化的呢?设想这样一种情况,在某一时刻,数据库崩溃了,mysqld进程异常退出,此时DBA将数据库重启,会发生什么事情?

这里就要引入一个叫做LSN的概念,即Log Sequence Number,可以理解唯一个序列号或者坐标。标记了日志或者数据文件中最新的位置。一般来说,磁盘文件中最新的LSN都会小于Redo中最新的LSN,那么这两个LSN之间的数据块,就是没有刷入磁盘数据文件的数据块了。

在崩溃重启后,数据库只需要将redo中这部分没有刷入磁盘的数据块刷到数据文件中就可以了,这样崩溃恢复的速度就会大大提高。

image.png
image.png

上图是redo文件的逻辑示意图,这里又引入了一个概念叫checkpoint,实际上是一个动作,即每次读取最老的脏页,确保这个脏页对应的LSN之前的LSN都已经写入了数据文件,这个脏页的LSN作为checkpoint点记录到日志文件中。write pos指写入的位置,也是一个LSN值,这两个LSN值之间的部分是可写部分,如果一旦write pos要赶上checkpoint了,就再做一次checkpoint。

至此我们已经讨论了redo log的一般套路,但是实际使用MySQL过程中还会遇到两个参数:

  • innodb_flush_method
  • innodb_flush_log_at_trx_commit

这两个参数是控制事务提交时的刷磁盘策略的,通常第一个参数在Linux系统上我们都会设置成O_DIRECT,至于这个O_DIRECT的实现方式,可以参考任何一本Linux内核编程的书,这里只需要知道这个O_DIRECT代表了不走OS cache,直接将缓存中的数据块刷到磁盘中。

下图说明了实际情况:

image.png
image.png

而第二个参数,则是控制了commit时redo刷盘的时机:

  • 1:每次commit,都会将redo buffer中的数据块写入redo并立刻刷磁盘;
  • 0:每隔一秒,将redo buffer中的数据块写入redo并刷盘;
  • 2:每次commit都会将redo buffer中的数据块写入redo log,但是每隔1s才会刷盘

2. Binlog

Redo其实是InnoDB特有的一种日志,这也是和Oracle学来的技术,因此学完了InnoDB以后再去学Oracle会感觉很轻松,因为基本原理都是一样的。

Binlog又是一种重要的日志,只不过这种日志是MySQL提供的,什么引擎都可以用。Binlog记录了数据的实际变更(当然如果binlog格式是mixed或者statement,那么大部分情况下记录的是SQL语句),当然有了这种日志,就能够实现复制功能了。而事实上大部分人用binlog都会去做复制,以及备份,当然也有很多人基于binlog开发了类似Oracle的闪回工具。

既然有了binlog,那么什么时候写binlog又是一个值得探究的事情,这就可以引出一个参数,sync_binlog,这个参数控制了binlog和事务提交的关系,取值范围是0-N:

  • 0:不去强制要求,由系统自行判断何时写入;
  • 1:每次commit的时候都要写binlog;
  • N:每N个事务,才会去写binlog

如此看来,最安全的选择还是1,加上之前的innodb_flush_log_at_trx_commit参数,如果选择最安全的方式也是1,在很多材料中都会写MySQL配置为双1,就是指的这两个参数的配置。

既然有两种日志,就会有一个写入的策略问题,这个问题也就引出了另一个概念——两阶段提交。所谓两阶段提交,其实就是将redo的提交拆分成了prepare和commit两个阶段,注意这里的commit不是commit语句,是一种状态。

当事务发起commit的时候,根据上面的描述,首先会将脏数据块写入redo log,但是此时还没有写binlog,因此阶段处于prepare阶段,只有当binlog完成了写操作之后,才会将redo log标记为commit,这个事务才算是真的完结了。

这时,我们来思考一个问题,如果写binlog的时候crash了,怎么办?因为redo log还是处于prepare状态,实际上事务没有真的commit,因此是会回滚的。

Binlog有一个比较好玩的参数:binlog_format,这个参数有三个选项,分别是ROW,STATEMENT和MIXED。

我最开始学习MySQL的时候看到这个MIXED便被他的名字迷惑了,这个参数一定是智能的,一定是最好的。但是事实证明我还是想错了,这个参数其实是为了兼容老旧的STATEMENT参数设计的,大部分情况下,binlog里记录都是STATEMENT格式。这里就要说一下STATEMENT格式记录了什么了。

如果是我来设计怎么将主库的事件发送到从库,那么我在设计时一定会首先想到将主库上执行过的所有的SQL都发给从库,这样就可以了。

这样的确可以,而且还很简单,但是确实有隐患。举一个简单的例子来说,如果主库上的SQL语句里有讲sysdate()插入表的语句,那么在从库上执行的时候,这个sysdate()获取到的实际上是从库的时间,这就存在主库和从库不一致的情况了,谁也不能保证语句立刻就能发送到从库并立刻执行,这一切操作保证在1s之内是很难的,存在网络问题,存在单线程复制回放问题的限制。

因此这种方式后来的MySQL开发者也觉得不好,所以设计了一种新的binlog格式,即ROW格式。这种格式记录了实际的数据变更,这样就解决了上面说的问题。不过,为了兼容旧版,MySQL的设计者设计了一个颇具迷惑性的MIXED选项,这个选项大部分情况下,都会把SQL语句记录在binlog里,而且这个选项也没有办法支持最新的GTID特性。

综上所述,一个系统要做复制,一定要使用ROW格式。

不过STATEMENT格式也不是没有其好处,至少binlog很好读,里面都是明文记录的SQL语句,要追查什么很方便,而ROW都是二进制加密的,可读性非常差,这里提供一个一般性的语句:

mysqlbinlog --base64-output=decode-rows -vv binlogXXXX

binlog是实现复制的重要日志,Master负责将事件记录在binlog中,并且通知slave将日志取走,Slave的IO线程将数据取走之后,将binlog中的事件保存在本地的中继日志中,由一个叫做sql线程的线程开始依次将中继日志中的事件回放在本地。

这个过程就是复制的基本原理,虽然现在有异步复制,半同步复制,增强型半同步复制,但是复制的基本流程和原理却始终没有变。

在此需要引入一个重要的概念——GTID,即全局事务ID。当然本文并不是论述GTID及其运维的,因此只是简单提要。

GTID的出现大大简化了基于binlog的复制配置和运维难度,配置复制的时候不再需要直接指定pos等信息,而是可以自动化的进行。

下图是binlog中记录的事件,可以看出来,每一个事务之前都会有一个set GTID的语句,因此从库在回放的时候,首先会执行该语句,那么这个GTID就会被从库维护起来,表示这个GTID已经执行过了。

image.png
image.png

假如我需要将slave节点挂载在主备上,那么基于GTID的复制会简化我的操作,因为slave上记录了所有已经执行过的GTID,此时是不需要手动干预去指定pos之类的值的,slave自己就可以判断要从哪里开始继续复制。

<a name="9d4fa2ba"></a>

3. Undo Log

Undo日志顾名思义是用来做回滚的,其实Undo的作用不止于此,MVCC这种重要的概念也是基于Undo实现的。

为了阐述MVCC,这里需要引入一个比较重要的概念——read view。我们知道,一个事务读取到的数据实际上是一个快照,这是MVCC的基本功能,只有这样,才能保证并发能力,即一个事务拿到记录的X锁之后,并不会阻塞其他事务读取数据,即便X锁和其他的锁是互斥的。

每一次的数据更新,都会生成一个read view,比如下面的图:

image.png
image.png

undo里面有按照顺序排列的read view,一个事务开始的时候,系统就会分给它一个read view,这样也就实现了MVCC。

那么谁去读当前值呢?看起来似乎每个读请求都是平行宇宙一样互不干扰,这就又引出了两个新的名词:

  • 当前读
  • 快照读

快照读读取到的就是read view,一般都会写的select一定是快照读了,但是如果你加上了for update,那么就一定是当前读了。

根据undo的生成机制,每次修改数据都会生成一个read view,那么在一个很多写操作的系统上,或者进行过大量批量修改数据的系统上,undo表空间就会变得非常大。在MySQL5.5版本以前,undo是放在共享表空间里的,而且这个共享表空间文件很有意思,一旦变大了就再也不会变小,虽然undo里的日志会被系统自动择机删除,但是其申请的空间就再也不会回缩了。

这是一个不好的设计,因此在后来的版本中,undo已经可以独立表空间文件了,而且这个文件也是可以回收空间的。

4. 其他日志

其他日志包括文本格式的error log,慢查日志和general log,还有中继日志。

其中中继日志顾名思义,就是将主库发送来的binlog先保存在本地,然后按循序进行回放。

文本格式的日志里,error log记录了MySQL运行过程中打印出来的日志,包括warning,error或者info,这些都是排查问题时的参考。

慢查日志记录了符合条件(慢查时间阈值,是否使用索引)的SQL,这是很重要的性能优化和排查依据。

general log一般用来调试的时候使用,记录了所有的数据库操作明细,开启以后会大量降低数据库系统性能,不建议在生产环境上开启。

5. 小结

MySQL的日志系统并不比索引,事务等系统简单,而且是十分重要的组成部分,也恰恰是因为日志系统的存在,MySQL或者说InnoDB才能保证用户数据的安全,保证较高的性能。

推荐阅读更多精彩内容