MySQL浅析之日志模块
1. 前言
我们都知道在MySQL中有一个重要的模块 – 日志模块。
其实在MySQL中,日志模块分为两种。
- 一种是记录Server层的日志,称为binlog。
- 一种是记录引擎层的日志,称为redo log ,这个日志是InnoDB引擎独有的。
2. 什么是redo log
假设我们要对一条数据进行修改,InnoDB是把数据从磁盘读取到内存的缓冲池上进行修改。这时,缓存数据与磁盘中的数据就不一致。我们该怎么做?没错,就是将缓存中的数据写回磁盘即可。但是,我们不难发现,用此方法的话,以后每对缓存中数据进行操作都要立马写回磁盘,这样还产生大量的IO操作,严重影响InnoDB的性能。那么InnoDB是如何处理的呢?我们不妨设想一下一个场景:
假设我们是一个贷款机构,有许多人都要在我们这进行贷款。这时,我们肯定需要一个账本来记录贷款人的记录。如果人少的话,我么当然可以直接在这个账本上进行修改等操作。但是,如果人一旦多了,我们就要每次都要翻这个账本进行修改。那么这个操作太麻烦了。
因此,我们可以单独弄一个小本本 – 用他来记录当天的贷款还款情况:
张三 贷款 5
李四 贷款 12
张三 还款 4
...
类似这样,记录当天情况,等不太忙时,最后进行汇总,记录到账本中。这样就可以大大简化操作。
而InnoDB就是使用这个方法,即WAL技术,WAL的全称是Write-Ahead-Logging,它的关键点就是先写日志,再写磁盘。即类似上一个案例中,先写在小本本中,等不忙的时候再写入账本中。
具体来说,当有一条记录需要更新的时候,InnoDB引擎就会先把记录写到redo log里面,并更新内存,这个时候更新就算完成了。同时,InnoDB引擎会在适当的时候,将这个操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做。
回到案例中,正当我们用此方法极大简化操作时,突然某一天,贷款人居多,我们的小本本都被记录满了,这时我们该怎么办?
没办法,我们只有放下手中的话。先将小本本的内容更新到账本中,再擦除小本本的记录,腾出空间,用来记录新的记录。
于此类似,**InnoDB的redo log是固定大小的,比如可以配置为一组4个文件,每个文件的大小是1GB,那么这个小本本总共就可以记录4GB的操作。从头开始写,写到末尾就又回到开头循环写,**如下面这个图所示。
write pos是当前记录的位置,一边写一边后移,写到第3号文件末尾后就回到0号文件开头。
checkpoint是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。
我们要始终记得,他是一个类似于环形的结构。
因此,write pos和checkpoint之间的是还空着的部分,可以用来记录新的操作。如果write pos追上checkpoint,表示空间满了,这时候不能再执行新的更新,得停下来先擦掉一些记录,把checkpoint推进一下。
有了redo log,InnoDB就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为crash-safe。
到此redo log 大概原理就是如此,下面让我们看看binlog吧!
3. 什么是binlog
我在前言中提到,binlog是Server层的日志。那么问题来了,有了redo log 为什么还有binlog?
其实是这么一回事:
因为最开始MySQL里并没有InnoDB引擎。MySQL自带的引擎是MyISAM,但是MyISAM没有crash-safe的能力,binlog日志只能用于归档。而InnoDB是另一个公司以插件形式引入MySQL的,既然只依靠binlog是没有crash-safe能力的,所以InnoDB使用另外一套日志系统——也就是redo log来实现crash-safe能力。
这两种日志有以下三点不同。
- redo log是InnoDB引擎特有的;binlog是MySQL的Server层实现的,所有引擎都可以使用。
- redo log是物理日志,记录的是“在某个数据页上做了什么修改”;binlog是逻辑日志,记录的是这个语句的原始逻辑,比如“给ID=2这一行的c字段加1 ”。
- redo log是循环写的,空间固定会用完;binlog是可以追加写入的。“追加写”是指binlog文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
看看区别,那么binlog我们可以用来干什么呢?
让我们注意一下这点:binlog是可以追加写入的。“追加写”是指binlog文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
没错,binlog的日志是不会被覆盖的。那么就简单了,比如说我们要将数据库恢复到一个月前的具体某一秒状态,该怎么做?
很显然用redo log 是不行的,因为它是会被擦除的。而使用binlog就没有这个问题了。
- 首先,找到最近的一次全量备份,如果你运气好,可能就是昨天晚上的一个备份,从这个备份恢复到临时库;
- 然后,从备份的时间点开始,将备份的binlog依次取出来,重放到中午误删表之前的那个时刻。
这样你的临时库就跟误删之前的线上库一样了,然后你可以把表数据从临时库取出来,按需要恢复到线上库去。
4. 两阶段提交
当用上这两个日志模块时,其实我们不难发现有一个重大的问题,就是如何保证数据的一致性。
这时,有可能不太懂了。别急,让我举个例子:
当我们更改数据时,肯定对这两个日志模块进行写操作,那么先写谁呢,会带来什么后果呢?
让我们分情况来看看:
我们有这么一张表
create table Student(id int primary key, age int);
我们再执行一条语句
update Student set age = age + 1 where id = 1;
将id为1的学生年龄加1。(我们假设age默认为20)
重点来了
先写redo log后写binlog
假设在redo log写完,binlog还没有写完的时候,MySQL进程异常重启。
因为写了redo log 因此恢复数据后, age进行了加1,得到21。但是由于binlog没写完就crash了,这时候binlog里面就没有记录这个语句。因此,之后备份日志的时候,存起来的binlog里面就没有这条语句。
之后问题就显而易见了,如果再某一天需要使用binlog恢复临时数据库的话,由于没有几句age加1这条语句,恢复后的数据中age的值为20,这样明显与我们的原数据是不一致的。
先写binlog后写redo log
如果在binlog写完之后crash,由于redo log还没写,崩溃恢复以后这个事务无效,所以这一行age的值依然是默认值20。
但是binlog里面已经记录了“把age加1”这个日志。所以,在之后用binlog来恢复的时候就多了一个事务出来,恢复出来的这一行age的值就是21,与原库的值不同。
因此我们需要一个解决数据一致的方法,那就是使用两阶段提交
将redo log 分为prepare和commit两个阶段,即为两阶段提交
5. 小结
我们了解了MySQL中的日志模块,即物理日志redo log和逻辑日志binlog。
redo log用于保证crash-safe能力,保证MySQL异常重启之后数据不丢失,以及减少IO操作,占用MySQL性能。
binlog用于数据备份,能够精确还原长时间前的数据。
最后介绍了两阶段提交,通过redo log 的两个阶段,用于保证数据一致性。