mysql重点Log三部曲第一部:redo log,接下来还有undo log和binlog,敬请期待
什么是Redo Log
在InnoDB存储引擎中,所有的操作都是以页为单位的。而在我们的客户端在进行数据的操作时,主要都会经过buffer pool这个缓冲池来完成,也就是说,真正访问页面之前,都需要把磁盘上的页缓存到buffer pool之后才可以访问。我们都了解事务有ACID四个特性,其中的C——持久性,说的是,已经提交的事务,在事务提交以后即使系统发生了崩溃,这个事务对数据库的更改也不可以丢失,但是试想,如果把数据直接读到buffer pool中,事务在提交后发生了故障,数据并没有及时同步到磁盘,内存中的数据丢失,这个就已经不满足于持久性了。这个时候可能会想到有以下这个解决方案:
- 在事务提交之前,把该事务涉及到修改的页面全部刷到磁盘中去
但是这个做法有一些问题:
- 将数据刷到磁盘中的基本单位是页,如果只是修改了某一行数据,也会将整个页刷盘,这个实在是很浪费
- 随机IO速度低。一个事务修改的数据可能并不在一个页里面,这些页面可能本身在物理上就不相邻,这种情况下,就会产生了大量的随机IO,需要经过一个不停的寻址过程,随机IO的效率比顺序IO低很多
我们想到的方式也给否定了,那该如何处理呢?再回到我们想说的问题:对于已经提交了的事务对数据库中数据的修改永久生效,即使是系统宕机,重启后也可恢复
那么这块在mysql中,对于一条修改的数据,就记录了这个数据哪些地方修改了来完成的。比如
update table set a = 1 where id = 1;
就会记录一条日志:
把第10表空间的第90号页面的偏移量为1024处的值更新为1
提交事务后,把这条操作日志刷到磁盘中,之后如果系统崩溃了,我们也可以找到这条日志完成对应数据的恢复。因此,上面的操作日志也被称为redo log。那么使用redo log的好处到底是什么呢?
- redo log占用的空间很小,而且可以通过参数进行动态设置。整体redo log占用的空间是一定的,并不会无线增大
- redo是顺序写入,比随机IO效率会高很多(顺序IO的文件通过预读方式能够大大的提升效率)
PS:保证事务持久性并不单单只有redo log,其实还有mysql的重要机制——double write,这块在讲完redo log后再说下
Redo log记录结构
下面是大部分类型的redo log的通用结构:
- type:redo log的类型,目前redo log的类型很多,下面会简单地提集中来了解
- Space ID:表空间ID
- page number:页号
- data:一条redo log的内容
展示一下源码的数据结构的样子
struct alignas(INNOBASE_CACHE_LINE_SIZE) log_t {
atomic_sn_t sn; // 目前log buffer申请的空间大小
aligned_array_pointer buf; // log buffer的内存区
Link_buf recent_written; // 解决并发插入Redo Log Buffer后刷入ib_logfile存在空洞的问题
Link_buf recent_closed; // 解决并发插入flush_list后确认checkpoint_lsn的问题
atomic_lsn_t write_lsn; // write_lsn之前的数据已经写入系统的Cache, 但不保证已经Flush
atomic_lsn_t flushed_to_disk_lsn; // 已经被flush到磁盘的数据
size_t buf_size; // log buffer缓冲区的大小
lsn_t available_for_checkpoint_lsn; // 在此lsn之前的所有被添加到buffer pool的flush list的log数据已经被flsuh, 下一次checkpoint可以make在这个lsn. 与last_checkpoint_lsn的区别是该lsn尚未被真正的checkpoint.
lsn_t requested_checkpoint_lsn; // 下次需要进行checkpoint的lsn
atomic_lsn_t last_checkpoint_lsn; // 目前最新的checkpoint的lsn
uint32_t write_ahead_buf_size; // write ahead的Buffer大小
lsn_t current_file_lsn; //
uint64_t current_file_real_offset; //
uint64_t current_file_end_offset; // 当前ib_logfile文件末尾的offset
uint64_t file_size; // 当前ib_logfile的文件大小
}
就不按照每一条说了,后面基本都会提到,没提到的大家可以自行了解一下~
Redo log类型
基础类型
redo log类型主要是通过上面记录中的type体现的。比较基础的有以下几个(基础的类似于java里面的基本类型):
- MLOG_1BYTE:type字段对应的十进制为1,表示在页面的某个偏移量处写入一个字节
- MLOG_2BYTES:type字段对应的十进制为2,表示在页面的某个偏移量处写入两个字节
- MLOG_4BYTES:type字段对应的十进制为4,表示在页面的某个偏移量处写入四个字节
- MLOG_8BYTES:type字段对应的十进制为8,表示在页面的某个偏移量处写入八个字节
- MLOG_WRITE_STRING::type字段对应的十进制为30,表示在页面的某个偏移量处写入一串数据
现在举一个例子。我们大部分情况下用的自增主键id都是int型或者是long型的,int为四个字节,long为八个字节,现在如果插入一条数据的话,这条数据实际是修改在buffer pool中的,然后通过redo log记录下当前的修改情况。那么这个时候,插入一条id(int)为9的数据的redo log应该是这样子的。
含义:在90表空间,编号为10页面,偏移量为1000处,写入四个字节,具体数据为0000 0000 0000 1001
复杂类型
对于正常的一条insert语句,不管这个表中有多少课索引树,都会将其更新,每更新一颗索引树,不光会更新叶子节点的页面,也很有可能会更新内节点的页面,甚至也有可能会新建页面。
而insert语句过程中对所有页面的修改都会记录到redo log中去,但是对页面更新的时候,不单单会只更新数据,还会更新File Header、Page Header、Slot(这块牵扯到页的结构,有兴趣的可以自己看看)等部分,因此在更新的时候再用上面的简单类型的redo log就不那么能满足需求了,也就是说,一条insert操作,一个页面修改的地方会异常地多,那么下面有几种方案:
- 每一处修改都记录一条redo log。这种情况的优势就是类型简单,记录简单,便于理解;但是弊端很明显,redo log记录太多,导致redo log占用了大量的空间,浪费资源
- 每个页第一处修改和最后一处修改当一条redo log中的具体数据。但是这种方案也有比较明显的缺点,第一处修改和最后一处修改中间有大量的未修改的记录,全部记录在redo log里面也是占用了大量无用的空间
基于这种情况下,InnoDB提出了一些新的redo日志类型:
- MLOG_REC_INSERT:type对应的十进制为9,表示插入一条使用非紧凑行格式的记录时的redo log类型
- MLOG_COMP_REC_INSERT:type对应的十进制为38,表示插入一条使用紧凑行格式的记录时的redo log类型
......
那么这些类型都是如何完成一条redo log的记录呢?通过MLOG_REC_INSERT举个例子
- type:MLOG_REC_INSERT
- spaceId:表空间id
- page number:页号
- record offset:当前记录的地址
- record length:当前记录长度
- info bits:表示记录头信息的前4个比特位的值以及record_type的值
- record origin offset:前一条记录的地址。(每向数据页插入一条记录,都需要修改该页面中维护的记录链表,每条记录的记录头信息中都包含一个称为next_record的属性,所以在插入新记录时,需要修改前一条记录的next_record属性。)
- mismatch index:为了节省redo log大小设立的字段,暂时可忽略
- record data:记录的真实数据
Mini-Transaction
redo log的写入方式
一条insert一局可能会涉及到若干棵B+树,这些修改直接都操作的是buffer pool,在buffer pool中修改完后,才会记录相关的redo log,而这些redo log是会被切分成不可分割的组(原子性)。比如:
- 修改聚簇索引时是不可分割的
- 修改二级索引时是不可分割的
那么具体这一层面的不可分割是什么意思呢?通过下面两个图理解一下
如上图所示,当前索引树一共有两个叶子节点,现在想插入一些记录,结果为下图
这个过程其实并不需要很复杂的处理,因为叶子节点空间是足够的,那么总有不足够的时候,要怎么办呢?每个叶子节点可以存放20条记录,可以看出第一个节点已经饱和了,那么我现在想再插入一个值为9.9的记录,该怎么办呢?
从上图可以看出,找到9.9的位置后,发现第一个页已经饱和了,这个时候就需要页分裂(页分裂一般会将之前页的数据量对半分配到两个叶子中),由于多了一个叶子节点,需要在对应的内节点中,多出一条记录,以便于索引,插入9.9记录后的索引树如下图所示
从上边的过程可以看出,插入过程中,不仅仅是对其中一个叶子节点会有改变,还可能会进行页分裂,对目录索引节点也会有改变,这些改变都会新增出很多redo log。这个时候就会有原子性问题。如果不保证这些redo log的原子性,好比说上边插入9.9的过程,叶子节点分裂出来了,但是目录索引节点并未多出目录项记录,这个就会导致通过redo log恢复崩溃前的系统状态这一操作出现错误。因此,redo log在某种程度上,必须能够保证原子性。那么是如何保证的呢?
————将这些要保证原子性的redo log放到一个组中
那么是如何放到这个组里面的呢?
————在该组的最后一条redo log后边加一条特殊类型的redo log,类型为MLOG_MULTI_REC_END(type的十进制为31),当系统崩溃重启进行恢复的时候,只有解析到类型为MLOG_MULTI_REC_END的redo log,才认为解析到了一组完整的redo,才会进行恢复,否则会抛弃前边解析到的redo log
那么这个时候可能会有疑问,redo log可能一组里面有很多redo log,可能只有一条,这种只有一条的本身就满足原子性,还需要在后边再加一个MLOG_MULTI_REC_END?这不是很浪费空间么?实际上不是这样的。对于redo log的type字段来说,一共占用了8个比特位,但是实际上解释redo log类型的,只是占了7个比特位,剩余的一个比特位,用来说明当前的redo log是一个单一的日志还是需要在一个组里面的redo log,如下图所示
如果第一个比特位为1,说明这是单一的一条redo log,否则表示这是个需要在组内保持原子性的redo log
Mini-Transaction
上边说的原子性地访问过程,称为一个Mini-Transaction。这个其实很形象,正常的事务为Transaction,但是这个是事务中的部分操作,是更细粒度的,因此叫做Mini-Transaction,也可以理解,一个Mini-Transaction包含很多redo log,映射关系如下
redo log的写入过程
redo log block
通过mtr生成的redo log都放在了页中(页的大小为512字节)。但是我们知道,在InnoDB存储引擎中,索引的最基本单位是页,索引中的页跟redo log存放的页是不同的,在这块我们称redo log存放的页为block,具体的结构图如下
- log block header存放管理信息
- log block body存放redo log
- log block trailer存放block的校验值,用于正确性校验
redo log buffer
我们也都了解过,InnoDB有一个Buffer Pool,是为了解决磁盘速度过慢问题,而衍生出的数据缓存池。那么对于redo log也是需要将数据写入到磁盘的,只要是将数据从内存写入磁盘,就肯定会面对一个问题:内存速度与磁盘速度的不平等性。而这个时候,大部分情况都会使用一个方案:在内存和磁盘中间加一层buffer。那么针对redo log写入磁盘的过程,也有一层redo log buffer。buffer中的基本数据单位就是上边提到的redo log block,其实很容易理解,因为redo log的基本单位是block,那么缓冲池基本上是要跟redo log存放的基本单位是一样的,也便于数据计算和统计。那么可以理解,redo log buffer的结构应该是下边这样的
redo log buffer的大小是可控的,innodb_log_buffer_size
通过这个参数可以设置,默认大小是16M
redo log写入redo log buffer
介绍完了存储redo log的数据结构,下面我们就介绍一下redo log是如何写入这层buffer的
redo log写入redo log buffer的过程是顺序的,也就是入redo log buffer的结构图所示,先写第一块block,写满了以后写第二块...以此类推。
现在有一条redo log数据,要写入buffer,我们会遇到第一个问题:这个数据要写在哪个位置?在InnoDB中提供了一个buf_free
的全局变量,来标识后续的redo log要写到哪里
还记得之前说过的mtr,一个mtr产生的多条redo log一定要保证原子性——不可分割,这块可以理解,redo log并不是产生一条就写入一条,而是说为了保证原子性,将一个mtr下的redo log一次性写入buffer。那么这里面就有一个问题了:每次mtr在完全产生redo log之前,每一条redo log放在哪里呢?其实这块只是暂时存放于内存非buffer的位置了,并没有特殊处理。
那么这个时候可能有人会问,那如果是两个事务,每个事务里面有多个mtr,这种情况下是不是就会产生同一个事务的redo log非连续的情况呢?
下面用例子来说明一下,多个事务,每个事务多个mtr是,redo log是如何写入的
现在有事务A,事务A下有mtrA_1和mtrA_2,现在有事务B,事务B下有mtrB_1和mtrB_2,看下每个事务产生redo log的情况
事务之间并非串行,大多数都是并行运行,也就是说,可能先产生了mtrA_1的redo log,然后产生了mtrB_1的redo log,再是mtrA_2的redo log,最后产生了mtrB_2的redo log,这种情况看下是如何在redo log buffer中分布的
可以看出,redo log buffer中的日志排列,其实就是根据mtr的生成顺序来排列的,而且可以将整个buffer理解为一个空间,一个mtr产生的redo log可以分布在多个block中,一个block也可以存入多个redo log组