Redo Log——第一篇

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的好处到底是什么呢?

  1. redo log占用的空间很小,而且可以通过参数进行动态设置。整体redo log占用的空间是一定的,并不会无线增大
  2. 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<byte, OS_FILE_LOG_BLOCK_SIZE> buf;  // log buffer的内存区
    Link_buf<lsn_t> recent_written;               // 解决并发插入Redo Log Buffer后刷入ib_logfile存在空洞的问题
    Link_buf<lsn_t> 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里面的基本类型):

  1. MLOG_1BYTE:type字段对应的十进制为1,表示在页面的某个偏移量处写入一个字节
  2. MLOG_2BYTES:type字段对应的十进制为2,表示在页面的某个偏移量处写入两个字节
  3. MLOG_4BYTES:type字段对应的十进制为4,表示在页面的某个偏移量处写入四个字节
  4. MLOG_8BYTES:type字段对应的十进制为8,表示在页面的某个偏移量处写入八个字节
  5. 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操作,一个页面修改的地方会异常地多,那么下面有几种方案:

  1. 每一处修改都记录一条redo log。这种情况的优势就是类型简单,记录简单,便于理解;但是弊端很明显,redo log记录太多,导致redo log占用了大量的空间,浪费资源
  2. 每个页第一处修改和最后一处修改当一条redo log中的具体数据。但是这种方案也有比较明显的缺点,第一处修改和最后一处修改中间有大量的未修改的记录,全部记录在redo log里面也是占用了大量无用的空间

基于这种情况下,InnoDB提出了一些新的redo日志类型:

  1. MLOG_REC_INSERT:type对应的十进制为9,表示插入一条使用非紧凑行格式的记录时的redo log类型
  2. MLOG_COMP_REC_INSERT:type对应的十进制为38,表示插入一条使用紧凑行格式的记录时的redo log类型
    ......
    那么这些类型都是如何完成一条redo log的记录呢?通过MLOG_REC_INSERT举个例子
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是会被切分成不可分割的组(原子性)。比如:

  1. 修改聚簇索引时是不可分割的
  2. 修改二级索引时是不可分割的

那么具体这一层面的不可分割是什么意思呢?通过下面两个图理解一下

初始化

如上图所示,当前索引树一共有两个叶子节点,现在想插入一些记录,结果为下图

新增20

这个过程其实并不需要很复杂的处理,因为叶子节点空间是足够的,那么总有不足够的时候,要怎么办呢?每个叶子节点可以存放20条记录,可以看出第一个节点已经饱和了,那么我现在想再插入一个值为9.9的记录,该怎么办呢?

新增9.9过程

从上图可以看出,找到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,如下图所示

redo log 结构

如果第一个比特位为1,说明这是单一的一条redo log,否则表示这是个需要在组内保持原子性的redo log

Mini-Transaction

上边说的原子性地访问过程,称为一个Mini-Transaction。这个其实很形象,正常的事务为Transaction,但是这个是事务中的部分操作,是更细粒度的,因此叫做Mini-Transaction,也可以理解,一个Mini-Transaction包含很多redo log,映射关系如下

事务、MTR和redo log关系图

redo log的写入过程

redo log block

通过mtr生成的redo log都放在了页中(页的大小为512字节)。但是我们知道,在InnoDB存储引擎中,索引的最基本单位是页,索引中的页跟redo log存放的页是不同的,在这块我们称redo log存放的页为block,具体的结构图如下

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

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要写到哪里

redo log buffer 写入数据

还记得之前说过的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的情况

事务、mtr和redo log的demo图

事务之间并非串行,大多数都是并行运行,也就是说,可能先产生了mtrA_1的redo log,然后产生了mtrB_1的redo log,再是mtrA_2的redo log,最后产生了mtrB_2的redo log,这种情况看下是如何在redo log buffer中分布的

redo log在buffer中的分布图

可以看出,redo log buffer中的日志排列,其实就是根据mtr的生成顺序来排列的,而且可以将整个buffer理解为一个空间,一个mtr产生的redo log可以分布在多个block中,一个block也可以存入多个redo log组

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容