Scott's world.

MySQL 日志系统笔记

Word count: 2.4kReading time: 8 min
2019/10/13 Share

MySQL 日志系统笔记

首先我们来以一条更新语句来开始我们的文章

执行更新语句前我们需要创建一个表(ID主键,c整型字段)

1
mysql> create table T(ID int primary key,c int);

若现在我们想要ID=3的这一行的值增加1

1
mysql> update T set c=c+1 where ID=2;

我们知道在MySQL执行一条语句需要经过以下步骤,我们通过一张图来了解

你执行语句前要先连接数据库,这是连接器的工作。

但是在一个表上有更新的时候,跟这个表有关的查询缓存会失效,所以这条语句就会把表T上所有缓存结果都清空。这也就是我们一般不建议使用查询缓存的原因。

然后分析器会通过词法和语法解析来知道这是一条更新语句

优化器经过最优抉择决定用ID这个索引

然后执行器负责具体执行,即找到这一行完成更新

与查询流程不一样的是,就是更新会涉及两个重要的日志模块

redolog(重做日志)

binlog(归档日志)

接下来就进入我们的主题

redolog

我们在执行更新一条数据的时候,我们一般会想到的流程是

  • 找到对应的那条数据
  • 然后更新那条数据

这时候就会出现的问题是,在整个过程的IO成本和查找成本都很高的时候,更新所花的代价是非常大的

同样MySQL也会遇到这样一个问题,而MySQL操作的地方就是磁盘,那么它是怎么解决这一个问题的呢?

大家肯定听说过缓存策略中的Write-Ahead,而用我自己的理解来说

接下来我也会写一篇关于缓存策略的文章来帮助大家理解

MySQL就是通过日志来执行这一个策略,也就是我们所说的WAL(Write-Ahead Logging)技术

关键点就是

先写日志,等系空闲的时候或者说日志空间满了的时候再写磁盘

而具体的过程是

  • InnoDB引擎就会把记录写到redolog中,并更新内存,这个时候更新就算完成了
  • 同时,InnoDB引擎会在适当的时候,将这个操作记录更新到磁盘中,而这个时候会根据具体情况来定比如空闲和已满

InnoDB的redolog是固定大小的,比如下面这张图,可以配置一组为4分文件

从头开始写,写到末尾就回到开头,这样循环下去

write pos是当前记录的位置,check point是当前要擦除操作的位置

擦除记录也就是把当前记录更新到数据文件

有了这个redolog,InnoDB就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力被称为crash-safe,因为在redolog里面有相应的记录,可以从里面提取出来,类似于备份

binlog

上面我们知道MySQL有两个部分:

  • Server层

    主要处理功能层面的事情

  • 引擎层

    负责存储相关的具体事宜

redolog就是InnoDB引擎特有的日志

除了引擎层有日志模块,在Server也有自己的日志模块—-binlog(归档日志)

我想你肯定会问,为什么会有两份日志呢?

因为最开始MySQL里并没有InnoDB引擎。MySQL自带的引擎是MyISAM,但是MyISAM没有crash-safe的能力,binlog日志只能用于归档。而InnoDB是另一个公司以插件形式引入MySQL的,既然只依靠binlog是没有crash-safe能力的,所以InnoDB使用另外一套日志系统——也就是redo log来实现crash-safe能力。’’

Binlog有两种模式,statement 格式的话是记sql语句, row格式会记录行的内容,记两条,更新前和更新后都有。

两者差异

  • redolog是InnoDB引擎特有的,而binlog是MySQL的Server层实现的,所有引擎都可以使用

  • redolog是物理日志,记录的是”在某个表中做了什么修改”

    而binlog是逻辑日志,记录的是语句的原始逻辑,比如”给ID=3这一行c列加1”

  • redlog是循环写的,空间固定

    而binlog是可以追加写入

    追加写入也就是文件写到一定大小后会切换到下一个,并不会覆盖以前的文件

更新语句的内部流程

了解以上两个重要的日志模块,我们再回过头来看看,开头所写的update语句,Server层即执行器和引擎层即InnoDB层是如何处理这一个语句的整个流程

1.执行器先找到引擎,引擎通过树搜索找到ID=2这一行.如果ID=2这一行所在的数据页本来就在内存中,就直接返回给执行器;否则需要先从磁盘读入内存,然后内存再返回

2.执行器拿到搜索到的这一行数据,把这个值加上1,得到新的一行数据,再调用引擎接口写入这行新数据

3.引擎将这行新数据更新到内存中,同时将这个更新操作记录到redolog里面,此时redolog处于prepare状态.记录完成后告知执行器执行完成,随时可以提交事务

4.执行器生成这个操作的binlog,并把binlog写入磁盘

5.执行器调用引擎的提交事务接口,引擎把刚刚写入的redolog改成提交commit状态,最后更新完成

其核心就是, redo log 记录的,即使异常重启,都会刷新到磁盘,而 bin log 记录的, 则主要用于备份。

这是给出update语句的执行流程图,浅色框代表引擎层,深色框代表Server层中执行的

这里我们可能不太理解为什么提交事务这一个步骤这么麻烦,即redolog处于prepare状态,要等到binlog记录完成并写入磁盘后才调用接口提交事务处于commit状态

prepare状态和commit状态,被称作”两阶段提交”

两阶段提交

为什么MySQL会采用两阶段提交这一个流程呢

目的很简单那就是保证两份日志之间的逻辑一致

那我们来说一说,如果我们想保证数据库恢复到上一个月某一条某时间的状态,应该如何操作?

这时候redolog和binlog就会发生巨大的作用

我们知道binlog会记录所有的逻辑记录,并且是”追加写入”的形式,如果你想恢复上一个月的数据那么最好备份中就会保存上个月的binlog,这就需要你定期做整库备份.

这里的”定期”取决于系统的重要性,备份的频率根据具体情况决定

假如某天中午12点你误删了一张表,先不用惊慌

  • 首先,找到最近一次的全量备份,假如是昨天晚上的一个备份,那么就从这个备份恢复到临时库
  • 然后,从备份的时间点开始,将备份的binlog依次取出来,重放到这天你误删表的前一时刻

这样你的临时库就跟误删表之前的线上库一样了,然后你就可以把临时库恢复到线上库中

回到为什么要进行这样的”两阶段提交”,如果我们采用不同的逻辑保存呢

仍然回到刚刚的update语句

假如当前行并没改变,并执行update语句过程中写完第一个日志后,第二个日志还没有写完期间发生了crash,会出现什么情况呢?

  • 先写redolog,后写binlog

    假设在redo log写完,binlog还没有写完的时候,MySQL进程异常重启。由于我们前面说过的,redo log写完之后,系统即使崩溃,仍然能够通过内存存入把数据恢复回来,所以恢复后这一行c的值是是更新后的值

    但是binlog没写完就crash,那么就没有记录相应的语句,从前面误删表的恢复例子我们可以知道若binlog不完全那么临时库就会不完全,就不能保证与线上库之前的数据一致

  • 先写binlog,后写redlog

    这个问题就更加的明显,由于redolog没有写,那么崩溃之后这个事务无效即c的值应该还是原值不应该发生改变,但是binlog里面已经记录了c的更新逻辑语句

    所以在用binlog进行恢复时就会多出来一个事务,还是会将值进行更新,这就与实际的值不符合

所以如果不使用“两阶段提交”,就会出现临时库与线上库数据不一致的状态

而使用”两阶段提交”就能保证逻辑上和数据上的一致性,因为任何一个阶段crash了

  • 若在commit状态前

    崩溃时直接将事务回滚,该事务就失效

  • 若在commit状态时

    崩溃恢复后会继续提交该commit状态

CATALOG
  1. 1. MySQL 日志系统笔记
    1. 1.1. redolog
    2. 1.2. binlog
      1. 1.2.1. 两者差异
    3. 1.3. 更新语句的内部流程
    4. 1.4. 两阶段提交