redo log的数据结构
之前我们已经对redo log的作用进行了初步的介绍,现在我们就要深入研究一下redo log的一些技术细节了。首先我们来看看写入磁盘上的日志文件的redo log大致长个什么样。
redo log里本质上记录的就是在对某个表空间的某个数据页的某个偏移量的地方修改了几个字节的值,具体修改的值是什么,它里面需要记录的就是表空间号+数据页号+偏移量+修改几个字节的值+具体的值。根据你修改了数据页里的几个字节的值,redo log就划分为了不同的类型,mlog_1byte类型的日志指的就是修改了1个字节的值,mlog_2byte类型的日志指的就是修改了2个字节的值,以此类推,还有修改了4个字节的值的日志类型,修改了8个字节的值的日志类型。当然,如果你要是修改了一大串的值,类型就是mlog_write_string,代表你一下子在那个数据页的某个偏移量的位置插入或者修改了一大串的值。所以一条redo log看起来大致的结构如下所示:
日志类型(就是类似mlog_1byte之类的),表空间ID,数据页号,数据页中的偏移量,
修改数据长度,具体修改的数据
有了上述信息,就可以精准完美的还原出来一次数据增删改操作做的变动了。
redo log是如何写入文件的
大家想一下,redo log是一条一条的直接写入磁盘上的日志文件吗?其实不是的,MySQL内有另外一个数据结构,叫做redo log block,你可以大概理解为平时我们的数据存放在数据页,那么对于redo log也不是单行的写入日志文件的,它是用一个redo log block来存放多个单行日志的。一个redo log block是 512字节,它分为3个部分,一个是12字节的header块头,一个是496字节的body块体,一个是4字节的trailer块尾。在这里面,12字节的header块头又分为了4个部分。
1、包括4个字节的block no,就是块唯一编号;
2、2个字节的data length,就是block里写入了多少字节数据;
3、2个字节的first record group,这个是说每个事务都会有多个redo log,是一个redo log group,即一组redo log,那么这个block里的第一组redo log的偏移量,就是这2个字节存储的;
4、4个字节的checkpoint on;
我们看下图:
所以我们看到上图就知道,对于我们的redo log而言,它确实是不停的追加写入到redo log磁盘文件里去的,但是每一个redo log都是写入到文件里的一个redo log block里去的,一个block最多放496字节的redo log日志。那么一个redo log block在日志文件里是怎么存在的呢?下面我们一起来看一下。
假设你有一个redo log日志文件,平时我们往里面写数据,一般都是从第一行开始,从左往右写,可能会有很多行。现在你要写第一个redo log了,先在内存里把这个redo log给放到一个redo log block数据结构里去,然后等内存里的一个redo log block的512字节都满了,再一次性把这个redo log block写入磁盘文件。我们看下示意图:
我们通过这个示意图就能很容易的理解redo log和redo log block的关系了。
redo log buffer
那么这个redo log 是如何通过内存缓冲之后再进入磁盘文件的,这就涉及到了一个新的组件,redo log buffer,它就是MySQL专门设计用来缓冲redo log写入的。这个redo log buffer其实就是MySQL启动的时候跟操作系统申请的一块连续内存空间,可以认为相当于是buffer pool吧。那个buffer pool是申请之后划分了N多个空的缓存页和一些链表结构,让你把磁盘上的数据页加载到内存里来的。
redo log buffer也是类似的,它申请出来一片连续内存,然后里面划分出了N多个空的redo log block。通过设置MySQL的innodb_log_buffer_size可以指定这个redo log buffer的大小,默认值是16MB,其实已经够大了,毕竟一个redo log block才512字节而已。到这里我们就明白了,redo log都是先写入内存里的redo log block数据结构里去的,然后才会把redo log block写入到磁盘文件。
万一redo log buffer里所有的redo log block都写满了呢?那此时必然会强制把redo log block刷入磁盘文件。另外,我们平时执行一个事务,每个事务会有多个增删改操作,那么就会有多个redo log,这个多个redo log就是一组redo log,每次一组redo log都是先在别的地方暂存,然后都执行完了,再把一组redo log写入到redo log buffer的block里去。如果一组redo log太多了,那么可能会存放到两个redo log block中。
但是反之,如果说一个redo log group比较小,那么也可能多个redo log group是在一个redo log block里的。下面我们来看一张示意图:
redo log buffer中的缓存日志什么时候写入到磁盘
现在我们来看看redo log block哪些时候会刷入到磁盘文件里去:
1、如果写入redo log buffer的日志已经占据了redo log buffer总容量的一半了,此时就会把它们刷入到磁盘文件里去。
2、一个事务提交的时候,必须把它的那些redo log所在的redo log block都刷入到磁盘文件里去,只有这样,当事务提交之后,它修改的数据才绝对不会丢失,因为redo log里有重做日志,随时可以恢复事务做的修改。
3、后台线程定时刷新,有一个后台线程每个1秒就会把redo log buffer里的redo log block刷到磁盘文件里去。
4、MySQL关闭的时候,redo log block都会刷入到磁盘文件。
上面几种redo log刷盘的情况,MySQL承载高并发请求的时候比较常见,比如每秒执行上万个增删改SQL语句,每个SQL产生的redo log假设有几百个字节,此时却是会在瞬间生成超过8MB的redo log日志,必然会触发立马刷新redo log到磁盘。不管怎么说,主要是保证一个事务执行的时候,redo log都进入redo log buffer,提交事务的时候,事务对应的redo log必须是刷入磁盘文件的,接着才算是事务提交成功,否则事务提交就是失败,保证这一点,就能确保事务提交之后,数据不会丢失,有redo log在磁盘里就行了。
当然,绝对保证数据不丢失,还得配置一个参数,提交事务把redo log刷入磁盘文件的os cache之后,还得强行从os cache刷入物理磁盘。
另外一个问题,当MySQL不停的产生大量的redo log,并写入日志文件,那么redo log就全部写入一个日志文件?对磁盘占用空间越来越大怎么办?
别担心,实际上默认情况下,redo log都会写入一个目录中的文件里,这个目录可以通过如下命令来查询:
show varibales like 'datadir'
可以通过innodb_log_group_home_dir参数来设置这个目录。然后redo log是有多个的,写满了一个就会写下一个redo log,而且可以限制redo log文件的数量,通过innodb_log_file_size可以指定每个redo log文件的大小,默认是48MB,通过innodb_log_files_in_group可以指定日志文件的数量,默认就2个。所以默认情况下,目录里就两个日志文件,分别为ib_logfile0和ib_logfile1,每个48MB。先写第一个,写满了就写第二个,如果第二个也满了就继续写第一个,覆盖第一个日志文件里原来的redo log就可以了。虽然只有96MB,但其实已经很多了,毕竟redo log真的很小,一条通常就几十个字节左右。如果你还想留更多的redo log,调节上述两个参数就可以了。
undo log回滚原理
我们现在来看一个问题,假设现在我们一个事务里要执行一些增删改的操作,那么必然是先把对应的数据页从磁盘加载出来放入buffer pool的缓存页里,然后在缓存页里执行增删改,同时记录redo log日志。但是现在问题来了,万一一个事务执行到了一半,结果就回滚事务了呢?这个时候就很尴尬,如果你要回滚事务的话,那么必须要把已经在buffer pool的缓存页里执行的增删改操作给回滚了。
毕竟无论是插入,还是更新,还是删除,该做的都已经做了。所以在执行事务的时候,才必须引入另外一种日志,就是undo log回滚日志。这个回滚日志记录的东西其实非常简单,比如你在缓存页里执行了一个insert语句,那么此时你在undo log日志里,对这个操作记录的回滚日志就必须是有一个主键和一个对应的delete操作,要能让你把这次insert操作给回滚了。
比如说你要执行的是delete语句,那么起码你要把删除的那条数据记录下来,如果要回滚,就应该执行一个insert操作把那条数据插入回去。如果执行的是update语句,那么要把你更新之前的那个值记录下来,回滚的时候重新update一下,把你更新前的旧值给更新回去。
下面我们通过一张图来加深理解,如下图:
insert语句的undo log回滚日志数据结构
insert语句的undo log的类型是trx_undo_insert_rec,这个undo log里包含了以下一些东西:
- 这条日志的开始位置
- 主键的各列长度和值
- 表id
- undo log日志编号
- undo log日志类型
- 这条日志的结束位置
我们来简单介绍一下,这个日志开始位置就没什么好说的了,那么主键的各列长度和值是什么意思?你插入一条数据,必然会有一个主键。有些情况下,主键可能是多个列组成的,比如“id+name+type”三个字段组成的一个联合主键。所以这个主键的各列长度和值,说的就是你插入的这条数据的主键的每个列,它的长度是多少,具体的值是多少。即使你没有设置主键,MySQL自己也会给你弄一个row_id作为隐藏字段,做你的主键。
接着表id,就是记录下来是在哪个表里插入的数据。undo log日志编号,这个意思是,每个undo log日志都是有自己的编号的。在一个事务里会有多个SQL语句,就会有多个undo log日志,它们的编号都是从0开始,然后依次递增。至于undo log日志类型,上面已经说过了。最后一个undo log日志的结束位置,就是告诉你undo log日志结束的位置是什么。下面我们来看一张图: