1. redo log

笔者以前认为MySQL的更新操作是这样的:每一次的更新操作就都要直接写进磁盘,要在磁盘中找到对应的记录,然后再更新,整个过程的IO成本、查找成本都很高。
但MySQL的做法是:更新记录的时候,InnoDB引擎会先把记录写到redo log里面,并更新内存,这个时候更新就算完成了。同时,InnoDB引擎会在适当的时候,将这个操作记录更新到磁盘里面,而这个更新通常是在系统比较空闲的时候做。但是如果数据多到redo log写满了呢?那就只有先把redo log中的一部分数据先更新到磁盘,然后清除掉redo log中对应的记录,再从擦除位置开始写。
以上的这个过程就是常说的WAL(Write-Ahead-Logging)技术,其关键点就是先写日志,再写磁盘。
图片.png InnoDB的redo log是固定大小的,如果配置为一组4个文件(默认是2个),每个文件大小是1G(默认是48M),总共就可以记录4G的操作记录。如上图所示,write pos是当前要记录的位置,边写边往后移,写到3号文件末尾又回到0号文件开头继续写。checkpoint是当前要擦除的起始位置,在擦除记录前要把记录更新到数据文件,然后往后推移并且也是可循环的。当write pos追上checkpoint,那么就要做写入磁盘和擦除数据的操作了。
InnoDB的redo log保证的是:即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为crash-safe。将innodb_flush_log_at_trx_commit参数设置成1表示每次事务的redo log都直接持久化到磁盘,也建议设置成1,保证MySQL异常重启之后数据不丢失。
redo log是InnoDB引擎特有的日志。

2. binlog

redo log是InnoDB引擎特有的日志,MySQL在Server层也有自己的日志,就是binlog(归档日志)。
redo log和binlog的不同之处:

  1. redo log是InnoDB引擎特有的;binlog是server层实现的,所有引擎都可以使用。
  2. redo log是物理日志,记录的是“在某个数据页上做了什么修改”;binlog是逻辑日志,记录的是这个语句的原始逻辑,比如“给id=2这一行的c字段加1”。
  3. redo log是循环写的,空间固定会用完;binlog是可以追加写入的。“追加写”是指binlog文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。

将sync_binlog参数设置成1表示每次事务的binlog都持久化到磁盘,也建议设置成1,保证MySQL异常重启之后binlog不丢失。

3. update语句执行时的内部流程

之前的流程和select语句没什么大的区别,到了执行器这里有些不一样。

  1. 执行器先找到引擎取id=2这一行。id是主键,引擎直接用树搜索找到这一行。如果id=2这一行所在数据页本来就在内存中,就直接返回给执行器;否则需要先从磁盘读入内存,然后再返回。
  2. 执行器拿到引擎给的行数据,把值加上1,得到新的一行数据,再调用引擎接口写入这一行新数据。
  3. 引擎将这行新数据更新到内存中,同时将这个更新操作记录到redo log里面,此时redo log处于prepare状态。然后告知执行器执行完成了,随时可以提交事务。
  4. 执行器生成这个操作的binlog,并把binlog写入磁盘。
  5. 执行器调用引擎的提交事务接口,引擎把刚刚写入的redo log改成提交(commit)状态,更新完成。

下图是update的执行流程图,浅色表示在InnoDB内部执行,深色表示在执行器中执行的。
图片.png

4. 两阶段提交

上图中最后三步将redo log的写入拆成了两个步骤:prepare和commit,这就是“两阶段提交”,主要是为了让两份日志之间的逻辑一致。
如果redo log和binlog不用两阶段提交,那么会出什么问题,仍然以前面的update语句为例:

  1. 先写redo log后写binlog。假设redo log写完,binlog还没写完的时候,MySQL异常重启。根据前面说的,redo log写完后,系统即使崩溃,仍然能够把数据恢复回来,所以恢复后该行值为+1后的值。但是由于binlog没写完就crash了,binlog里面就没有记录这个语句。因此,之后备份日志的时候,存起来的binlog里面就没有这条语句。如果需要用这个binlog来恢复临时库的话,由于binlog中该语句丢失,临时库就会少一次更新,恢复出来的值就是+1前的值。与原库值不同。
  2. 先写binlog后写redo log。如果在binlog写完之后crash,redo log还没写,崩溃恢复以后这个事务无效,所以这一行的值还是+1前的值。但是binlog里面已经记录了“把c值从0改为1”这个日志,所以之后用binlog恢复的时候就多一个事务出来,恢复出来的c值就是1,与原库的值不同。

搭建备库增加系统读能力的时候,现在常见的做法也是用全量备份加上应用binlog来实现的,如果两个日志文件不一致,就会导致线程出现主从库不一致的情况。