(六)MySQL日志篇2:MySQL的日志是做什么用的?(redo log, undo log, binlog)

2024年 1月 12日 81.7k 0

一篇文章讲清楚MySQL的日志作用

1. undo log

2. redo log

3. binlog

前提概要

在日志篇的开头,我们详细分析了一条sql是如何执行的,搞明白这个,是理解MySQL三大日志(redo log, undo log, binlog)的基石,感兴趣的小伙伴可以看看这篇博客 一条sql是如何执行的?

在本篇章,三大日志是我们的重点,同时还有一些辅助日志的介绍,整体的架构如下图!

image.png

undo log存在的意义

undo log是什么?它具体放在哪里?作用是什么?

undo log的作用

要搞明白undo log的作用,我们首先要清楚undo log日志里都记录了什么, 为了方便理解,我们可以这么认为:

  • 当执行insert语句时,undo log会记录对应的delete语句
  • 当执行delete语句时,undo log会记录对应的insert语句
  • 当执行update语句时,undo log会记录反的update语句,例如:执行的是 update A to B,记录的是 update B to A。

因此,undo log的作用也呼之欲出了。

作用1:保证事务的原子性

如果一条事务里有两条update语句,执行了一条崩溃后,MySQL便可以通过undo log日志来找到旧内容,做回滚。

作用2:实现MVCC的关键因素

我们知道,一条记录如果对其不断地做修改,是会存在多个版本的,所有的版本记录都是会记录在undo log日志中,会通过指针把版本记录串成链表,来实现MVCC。具体的MVCC分析:MVCC是如何解决事务并发问题的? 在此不反复叙述。

但其实本质上,undo log记录的不是相反的sql语句(在回滚的时候执行),记录的是被老版本的数据。可以从行数据列存在的隐藏列(roll_pointer)回滚指针来理解:

  • 当执行一条insert语句时,这条记录是第一个版本,因此roll_pointer指向NULL。当发生回滚时,直接通过回滚指针,用NULL来覆盖记录,达到删除记录的作用。
  • 当执行一条delete语句时,同样的,将数据页中的该列的deleted置于1(后续会细讲),再将roll_pointer指向删除前的数据,也可通过指针回滚。

因此,undo log里其实记录的是一堆老数据,通过roll pointer来实现回滚的功能,强烈建议看下这篇文章,undo log配合MVCC食用,理解效果更好。MVCC是如何解决事务并发问题的?

undo log存放在哪里?

对于InnoDB引擎创建的表,每张表在磁盘中都有两个文件,xxx.frm,和xxx.ibd。frm文件中存放了表的结构,类型。而ibd文件主要存放了表的数据以及索引相关信息。 并且,ibd中还有一片叫“段”的区域,一张表有128个回滚段,一个回滚段有1024个segment。一条undo log记录对应一个segment。

image.png

undo log相关的配置参数

有几个比较重要的undo log参数,需要有点印象

innodb_undo_directory: 指定undo log的存放路径,默认放在ibd文件

innodb_undo_logs: 指定undo log的回滚段的数量,默认为128个

redo log详解

redo log是什么? 为什么需要redo log?redo log是如何工作的?redo log的内存结构?

redo log作为InnoDB引擎特有的日志,也正是因为redo log,让MySQL拥有数据不丢失的能力。(这个也算是数据库的基本保障把,没有还做啥数据库?)该日志也是我们学习MySQL进阶的必要内容

redo log是什么?

假设存在一场景,古代有个老铁开了间客栈,老铁兼任客栈的账房先生,需要记录来客栈的每一位顾客的赊账记录。

假设这时候有生意上门了,有顾客要找老板赊账,此刻老板有两种做法:

  • 拿出账本,找到该顾客的记录,把赊账的记录更新到账本里面,
  • 先用一个临时的本子将记录记下,后续再一起更新。

image.png

在生意很忙的时候,为了效率最大化,脑子不笨的老板肯定是会选择做法2。最后在不忙的时候一起更新到账本里(打烊的时候,或者顾客较少时)。

image.png

把这个例子套用到MySQL的增删改语句中,redo log就等同于临时的本子!

为什么需要redo log?

在MySQL中同样有这个问题,如果每次的数据变更操作(顾客的赊账记录)都要写到磁盘里(账本),都需要先查找记录,再更新记录,最后再回写。一读一写,IO成本太高,影响效率。

因此,MySQL是这样做的:

  • 对于每一笔赊账记录,老板先将这笔帐记录在脑子里(记录写到内存)
  • 再写到临时的本子上(redo log日志)
  • 最后在不忙的时候,更新到账本里(IO操作刷盘)

通过三步走,以此来提高效率。这也是著名的WAL技术。并且,日志会比数据先写到磁盘里(写日志也是一种IO操作),提供了系统崩溃的时候能保证数据不丢失的能力(crash-safe)后续展开描述

Tips: 记录到redo log的内容为物理日志。例如:在xxxx页做了xxxx修改。而非逻辑日志,直接记录sql

image.png

redo log在磁盘中以二进制的格式存在,默认的文件名为ib_logfile(0,1,2....),具体生成的文件个数和大小均可以配置,默认为2个文件,大小为48M(1024 * 48 = 49152K),如下:

image.png

redo log是如何工作的?

写redo log日志的形式是顺序写,直接追加记录在日志的末尾即可。顺序写表明了数据会不断地被覆盖,具体的架构图(顺时针记录,配置了4个redo log日志)如下:

image.png

架构的主要成员如下:

  • 指针write pos;表明当前记录的位置,往前推说明有记录被写到redo log了
  • 指针check point:表明当前要持久化的位置,往前推说明有记录从redo log持久化到磁盘了
  • 区域write pos:表明redo log还可以写入的空间,为两根指针中间。

当一条sql被执行时,会先将记录写到redo log的末尾(顺序写),即指针write pos会往前推;写完redo log后会返回执行成功,此刻还没有真正入盘。当这条记录被指针check point追上时,表明这条记录被持久化到磁盘了。

于是乎,执行一条数据变更的sql本是要去磁盘做随机写的(得先磁盘寻址,判断,再写入),通过redo log,变成了日志的顺序写。从操作系统的角度出发,同样是写操作,随机写的效率是要远小于顺序写的

image.png

redo log是如何保证crash safe能力的?

由前面可知,MySQL执行sql后的数据变更并不会马上写到磁盘里,而是会先记录到内存,并写redo log日志,若此刻突然断电,本应该持久化的数据又如何保证不丢失呢? 结合redo log的架构,我们来尝试“还原数据”,步骤如下:

  • 先定位到redo log日志中的双指针(write pos、check point)位置
  • check point指向的位置,其实就是当前MySQL从内存写到磁盘的位置,左边是redo log可写入的区域,右边是已经记录在redo log,但未刷盘的数据
  • 此刻,从check point往前推进直到和write pos相遇之间的环形数据,均是之前存储在内存的数据,但未刷盘的。
  • 因此,MySQL只要将check point推进到和write pos一样的位置,就能将之前在内存的数据(未写盘的)给持久化到磁盘上,完成“丢失数据的恢复”。
  • redo log是如何刷脏页的?(check point的前移)

    什么是脏页?

    当内存的数据页和磁盘的数据页内容不一致时(sql已执行,内存存放最新数据,但redo log还未写到磁盘),将这一内存页称之为脏页

    check point肯定不会一直原地不动的,也会主动刷盘,将数据持久化到硬盘。移动的时机总结如下:

    • 临时的本子写满了,需要将本子的记录擦掉一些。(对应的是redo log写满了,write pos的空间为0,此时需要将check point往前推一推)(尽量避免,redo log若写满,所有的写操作都会被阻塞,严重影响性能)
    • 生意太好了,老板的脑子已经记不下东西。(对应的是内存不足,需要新的内存页 -> 淘汰掉部分页;如果淘汰的是脏页,就需要推动check point)(影响性能)
    • 生意不忙的时候,老板闲着也是闲着,把本子拿出来更新下账本。(对应的是MySQL的读写压力不大,redo log主动推进check point)(该情况对性能的影响不大)
    • 店面要打烊了,关门。(对应的是MySQL的进程关闭,需要把redo log清一清)(进程都要关了,对性能的要求也不大)

    总结如下:

    image.png

    MySQL是怎么主动刷脏页的呢?频率又该如何计算?

    刷脏页的本质上就是做IO操作,因此有一个参数来控制:innodb_io_capacity,系统的默认值为200。表明告诉MySQL每秒刷200个脏页。

    1704804728745.png

    这个值是特别重要的,变相告诉了MySQL你这台主机的IO能力。值越低,刷脏页的速度越慢。如果刷脏页的速度小于redo log写入的速度,就会触发MySQL去频繁的被动刷脏页,影响性能。 因此,该值最好是直接设置成你主机的IOPS(每秒的读写次数),可以通过如下命令去检测:

    fio -filename=$filename -direct=1 -iodepth 1 -thread -rw=randrw -ioengine=psync -bs=16K -size=500M -numjobs=10 -runtime=10 -group_reporting -name=mytest
    

    在我本机执行如下:

    image.png

    我自己的电脑的IOPS是4161,因此可直接将innodb_io_capacity设为4000.

    但即使是你将innodb_io_capacity设为4000,MySQL也不可能全力去按4000的速度刷脏页,毕竟还需要提供其它服务,不止是刷脏页,具体的速度计算如下:

            V(m) = if (m > innodb_max_dirty_pages_pct)
                        return 100;
                   else
                       return 100 * m / innodb_max_dirty_pages_pct;
    

    1704806951140.png

    上述公式:m代表当前的脏页比例;innodb_max_dirty_pages_pct代表脏页比例上限(默认是75%)。因此,该公式的含义为:当系统的脏页比例大于设定的阈值,开始全力刷脏页!

    当前的脏页比例通过:show global_status获取,如下:

    image.png

    对于MySQL来说,无时无刻充满了各种优化,包括这个刷脏页的策略。MySQL存在一个优化策略,由innodb_flush_neighbors参数控制,默认是开启的

    当该参数开启时,刷脏页会有连坐机制,如果此刻要刷的脏页的邻居也是脏页,会顺手一起刷掉,并且还可以蔓延,邻居的邻居也可以覆盖。 通过该手段,减少了大量随机IO,也能整体带动系统性能

    redo log的刷盘策略(写入时机)

    在执行sql,写redo log日志时,也不是直接把redo log直接写到磁盘的,而是写到了一个缓存池buffer里。因此,redo log把buffer的数据刷到磁盘里,也是存在对应的策略的。

    PS:这里的刷盘策略不同于上面的刷脏页策略。刷脏页策略的本质是将行数据持久化,而刷盘策略的本质是将redo log日志持久化

    redo log中的一条记录的状态可能会经历三阶段,如下:

    image.png

    一、 在开始阶段,记录先被写到redo log buffer中,此刻该记录存在MySQL的进程内存中,未被持久化。(如果MySQL进程崩溃,数据可能会丢失)

    二、记录从buffer中写到(write)到系统中的page cache,对应的是操作系统。(已经从MySQL转移出来,但此刻服务器若宕机,数据仍有丢失的风险)

    三、记录从page cache中持久化(fsync)到磁盘,对应的就是彻底持久化。(此刻服务器宕机或者MySQL崩溃,数据都不会丢失)

    具体的持久化策略由参数:innodb_flush_log_at_trx_commit控制。具体策略如下:

    • 当设置为0时,表明每次事务提交时,redo log只会被记录到redo log buffer中(极度不安全)
    • 当设置为1时,表明每次事务提交时,redo log会直接fsync到硬盘中(最安全)
    • 当设置为2时,表明每次事务提交时,redo log会被write到page cache中

    除了由参数控制的主动刷盘策略,redo log还存在被动的刷盘策略,redo log buffer是存在于buffer pool中的一块内存空间,是线程共享的,(有点类似JVM中的堆空间):

    • InnoDB存在一后台线程,每隔一秒,会把redo log buffer中的数据全都fsync到磁盘。(表明redo log buffer中的数据不会呆超过1s)
    • redo log buffer占用的空间达到innodb_log_buffer_size的一半,后台线程就会清场,把数据都write到page cache中
    • 事务提交时,write或者fsync的最小颗粒度是整个redo log buffer。因此,可能会顺带将其它事务的redo log一起持久化

    无论是被动还是主动,总结如下:

    image.png

    redo log buffer结构剖析

    上面我们详细介绍了redo log的刷盘策略(离不开redo log buffer),redo log buffer由无数个redo log block组成(可以简单将block理解成一个page),总大小为512字节。

    image.png

    而一个block的具体结构如下:

    image.png

    log block header: 存放这个block的一些基础信息(标识符,body的具体占用字节,check point的序号等),占12B

    log block body: 存放具体的redo log记录,占496B

    log block tail: check_sum,校验和,验证这个block是否完整,在各大协议中校验和也是比较常见的,占4B

    redo log相关的配置参数

    有几个比较重要的redo log参数,可能会在工作中用到,需要有点印象

    innodb_flush_log_at_trx_commit: 指定redo log的刷盘策略,默认为1

    innodb_log_group_home_dir: 指定redo log的默认保存路径,默认在根目录下

    innodb_log_buffer_size: 指定redo log buffer的大小,默认为16M

    innodb_log_files_in_group: 指定redo log日志的磁盘文件个数,默认为2

    innodb_log_files_size: 指定redo log日志的每个磁盘文件的大小,默认为48M

    innodb_io_capacity: 指定MySQL刷脏页的速度,默认为200

    binlog详解

    binlog是什么?binlog的作用?有了binlog为什么还要redo log??

    当我们提到binlog时,往往是拿redo log来跟它做比较。同样都是记录数据,也都有刷盘策略;下面我们一步一步来揭开binlog

    什么是binlog?

    不同于redo log(InnoDB引擎独有),binlog是MySQL原生的日志。和redo log日志类似,binlog也是用来记录数据,但没有redo log复杂的双指针模式,binlog采用追加写的形式,不会覆盖之前的数据,当文件写满时,会创建新的文件继续写,如下:

    image.png

    在磁盘中以二进制的形式存在,初始文件名为mysql-bin.0000001。随着文件的增多,后面数字不断递增。由于其追加写的形式,binlog主要应用于数据备份,主从库间的复制

    在MySQL中,binlog默认是关闭的,可通过 log_bin 查看情况,需要往mysql的配置文件中添加如下配置:

            [mysqld]
            # 开启binlog
            log_bin=ON  
            # binlog的记录形式(后续重点)
            binlog_format=mixed
            # binlog的保存路径
            log_bin=/var/lib/mysql/mysql-bin
            # mysql的id(在集群模式下需唯一<img src="https://juejin.cn/post/)" alt=""   />
            server-id=123
            # 存放时间
            expire_logs_day=30
            # 单个文件的最大大小
            max_binlog_size=50m
    

    将如上的配置加上后,重启MySQL,即可看到如下,表明binlog日志已开启。

    image.png

    记录binlog的三种格式介绍

    在上面的binlog配置项中,其中有一项是binlog_format,代表binlog在记录数据时的数据格式,有三种格式,分别是statement,row,mixed

    为了方便区分三种格式,我初始化了一些数据,一张user表,存在字段id,name,age,male,数据如下:

    image.png

    statement

    当binlog_format=statement时,binlog里记录的就是sql语句的原文,执行一条sql语句如下:

                        delete from user where id > 2 limit 2; (执行)
    

    执行完毕后,可通过:show master status; 获取当前正在写的binlog日志

    最后,通过如下命令,直接查看binlog日志的最底部

                        show binlog events in `mysql-bin.000018`;
    

    结果如下:

    image.png

    结果如上图,binlog里直接记录了真实执行的sql原句,加上数据库,以及事务的开始和结束指令。

    row

    当binlog_format=row时,binlog里记录的是数据的全量信息,再执行相同的sql,通过命令查看到:

    image.png

    原来的sql语句已经没有了,取而代之的是记录了table_map和delete_rows,其中:

    • table_map: 记录了具体操作的数据库和表
    • delete_rows: 定义了删除数据的行为

    并且可以看出,删除数据的行为是在binlog的第844行开始的。因此,我们借助原生的mysqlbinlog命令了解具体信息,如下:

                mysqlbinlog --no-defaults -vv mysql-bin.000019 --start-position=844
    

    结果如下:

    image.png

    对比row和statement

    对比两种不同的格式,我们可以发现有以下区别:

  • 当格式为statement时,binlog记录的是原始的sql语句,如果sql语句包含limit,自增主键,时间戳(now())等条件,很有可能会出现主备不一致的问题,导致数据不一致。

    如下,在执行完sql,通过:show warnings; 可看出:

  • image.png

  • 当格式为row时,binlog记录的是全量的语句。如果碰到批量删除 or 增加的sql,例如:用一条delete语句删除10W条数据,binlog对应的会记录10W条记录,(statement只会记录sql)。会占用极大的空间
  • mixed

    由于statement和rows都有各自的优缺点,无法很准确地定义出谁好谁坏。因此,MySQL取了一种折中的方案,mixed格式出现了

    我们害怕statement产生的主备不一致问题,又害怕row产生的空间占用率大的问题。因此,当binlog_format=mixed时,把选择权交给MySQL,让其自行判断这条sql语句是否会产生主备不一致的问题。如果会,就采用row格式记录数据,反之则采用statement格式记录数据。

    binlog的刷盘策略(写入时机)

    MySQL在写redo log的时候,是会经过redo log buffer -> page cache -> disk的过程的。同样的,binlog也是类似,MySQL在写binlog时,同样不会直接将其写到硬盘,一条记录可能的状态如下:

    image.png

    一、 在开始阶段,记录先被写到binlog cache中,此刻该记录存在MySQL的进程内存中,未被持久化。(binlog cache是线程私有的,非共享。这点和redo log buffer有区分)

    二、记录从buffer中写到(write)到系统中的page cache,对应的是操作系统。

    三、记录从page cache中持久化(fsync)到磁盘,对应的就是彻底持久化。

    为什么binlog cache是线程私有的?

    我的理解是binlog的主要功能为数据备份,需要存储的是全量数据;执行的时候肯定是要按一个完整的事务来的,不能穿插。(因此,每个执行事务的线程都需要有独自的cache)。如果搞成共享的,极有可能会出现向redo log那样被动提交的情况。

    具体的持久化策略由参数:sync_binlog控制。具体策略如下:

    • 当设置为0时,表明每次事务提交时,binlog只会write到page cache中
    • 当设置为1时,表明每次事务提交时,binlog会直接fsync到硬盘中(最安全)
    • 当设置为N时,表明每次事务提交时,binlog只会write,累积N个事务后才会一起fsync

    Tips: 一般我们使用binlog,sync_binlog都是置为1。但是当数据库出现IO瓶颈时,可以临时将其设置为N,减小IO的压力。但面临的危险是,一旦服务器崩溃,可能会丢失最近N个事务的记录。

    binlog做数据恢复

    记录几个比较简单的数据恢复命令,按时间 or 按具体的行数,全量复制的命令如下:

                mysqlbinlog --database=xxx mysql-bin.0000xx | mysql -uxxx -pxxx
    

    按具体行数,可添加参数如下:

                        --start-position=xxx      --end-position=xxx
    

    按时间命令,可添加参数如下:

                        --start-datetime=xxx (timestamps or datetime)
    

    有了binlog,为什么还需要redo log?

    这个真的是一个非常经典的问题了。出于常识,MySQL不会吃饱撑着,所以肯定是两者都需要的,侧重点不同。redo log让MySQL拥有断电不丢失数据的能力;binlog让MySQL拥有数据备份的能力

    可以没有redo log,只有binlog吗?

    既然binlog全量记录了数据,那可以将redo log拿掉,用binlog来做断电恢复数据吗?

    这肯定是不行的,如果MySQL突然发生异常重启,仅靠binlog是没办法保证数据不丢失的,理由如下:

    • MySQL做的数据变更操作,为了效率,是不会马上写到磁盘,而是会保留在内存的。
    • 由于binlog是追加写,没有像redo log那样拥有双指针,可以准确定位到哪部分的数据还没有写到磁盘,就像无头苍蝇一样,也不可能把整个文件都跑一遍。
    • 因此,宕机前存储在内存的数据(未持久化的那一块),会丢失。

    可以没有binlog,只有redo log吗?

    这也是不可能的,因为redo log是顺序写,像绕圈一样,当前的记录会覆盖掉过去的记录,没办法做到数据的完整记录,不具备像binlog那样的归档能力。

    binlog和redo log区别总结

    总结如下:

    image.png

    辅助日志介绍

    slow log

    slow log,即慢查询日志。当一条sql执行时间超过设定的阈值时,该sql就会被记录在slow log中

    假设执行一条sql: select sleep(10); 休息10s,慢查询日志内容如下:

    image.png

    慢查询日志中,会记录具体的sql,以及花费的查询时间(Query_time)和加锁时间(Lock_time)

    slow log的相关配置参数如下:

    slow_query_log: 设置是否开启慢查询日志,默认值是OFF,关闭的

    show_query_log_file: 指定慢查询日志的存储目录及文件名,默认是放在具体数据库的目录下

    long_query_time: sql执行时间的阈值,默认值是10s,(slow log在MySQL中是没有缓存的,会实时的IO写盘,因此要合理设置该值,避免IO压力)

    error log

    错误日志,用于记录系统的报错信息,包括MySQL进程的启动,以及停止等等

    截图部分error log,同时还会记录MySQL运行的各种信息,如下:

    image.png

    error log的相关配置参数如下:

    log_error: 指定错误日志的存储目录以及文件名,默认值是放在data目录下

    general log

    general log,用于记录MySQL执行的查询日志,无论结果如何,都会记录

    截图部分general log,如下:

    image.png

    general log直接记录sql,逻辑记录

    general log的相关配置参数如下:

    general_log: 是否开启查询日志,默认值是OFF,关闭的(为了性能,默认是关闭)

    general_log_file: 指定查询日志的存储目录及文件名,默认值是放在data目录下

    relay log

    中继日志,仅存在主从架构的从机上,日志存在的内容,形式,架构和binlog日志保持一致。具体的形式如下:

  • 在主从架构数据同步时,从机会接收主机的binlog日志数据。
  • 接收的数据会放在从机的relay log内。
  • 从机有专门的线程池去消费relay log的内容,完成主从复制。
  • 详解二阶段提交

    再回到我们一开始的问题,我们学习redo log, binlog, undo log日志,从某种意义上就是为了让我们能更好的理解一条sql的具体执行过程。而最后一步,就是二阶段提交

    详细的内容如下:

    image.png

    二阶段提交的基本流程

    基于前面对redo log和binlog的详解,我们进一步细化执行一条update语句在内存更新后的流程,大致的流程可以参考下这篇文章一条sql语句是怎么样执行的?

    具体的流程图如下:

    image.png

    二阶段提交!即将redo log的写入拆成了prepare, commit两个状态

    一、将新数据写到内存后,执行器调用innoDB引擎提供的接口,先记录undo log日志。

    二、执行器调用innoDB引擎提供的接口,将物理记录写到redo log日志内。此刻,redo log处于prepare状态,并告诉执行器执行OK。

    三、执行器将记录写到binlog日志内。写完后,调用innoDB的接口,将redo log日志的状态从prepare变为commit。

    总结来说 -> 二阶段,就是把redo log的写入拆成两步:prepare和commit,中间穿插写binlog

    prepare阶段:将执行该条sql的事务XID写到redo log,根据设定的刷盘策略,写入redo log,然后将redo log日志的状态设为prepare。

    commit阶段:执行器写binlog(包含事务的XID),而后调用innoDB提供的提交事务接口,将redo log日志的状态设为commit。(此commit并非是事务提交的commit状态,事务的commit状态标识了这个事务的完整结束;而二阶段提交的commit阶段是代表了当前事务的记录被完整记录到binlog和redo log中)

    为什么需要二阶段提交?

    为什么MySQL要把一个提交过程搞得这么复杂呢?肯定是有原因的,就是保证数据的一致性,若发生半成功现象,必然导致数据不一致!

    假设没有二阶段提交,先写redo log,再写binlog,如下:

    image.png

    如果在写完redo log,在时刻A宕机了。此刻,binlog尚未写入,MySQL重启的时候可以通过redo log还原断电丢失的数据。但在主从复制的时候,由于binlog缺失数据,就会出现主备不一致的问题

    反过来,先写binlog,再写redo log,如下:

    image.png

    如果在写完binlog,在时刻A宕机了。此刻,redo log尚未写入,MySQL重启的时候可以由于redo log尚未写入,原来的数据无法恢复。但在主从复制的时候,由于binlog没有丢数据,也会出现主备不一致的问题

    因此,二阶段提交还是很有必要的!

    二阶段提交时,系统崩溃了...

    再回顾一下完整的提交流程,

    image.png

    下面我们来看下,有二阶段提交的前提下,系统崩溃了会发生什么现象?

    在时刻A及以前崩溃,此刻redo log里面有数据,binlog里未写入。因此,在系统恢复后,该事务必然是回滚的。

    关键是在时刻B崩溃了,此刻的redo log其实已经写入了,是否回滚的关键在binlog。判断逻辑如下:

    • 如果此刻执行器已经调用了提交事务的接口(redo log的状态为commit),此刻binlog日志已写入,而redo log的状态也over,事务直接提交!
    • 如果此刻执行器还未调用提交事务的接口,redo log的状态还是prepare。则需要判断:对应的binlog日志中,是否有commit的内部事务XID。
      • 如果有,则事务提交!
      • 如果没有,表明binlog未完整写入,事务回滚!

    如下:在binlog日志的最后一行,事务的commit阶段都是会带上XID的。

    image.png

    回滚的关键是查看binlog是否完整! 具体的判断流程如下:

    image.png

    二阶段提交引起的问题

    如果是在双1配置下(redo log和binlog的刷盘策略都为1),每次redo log和binlog的更新,都会强制将数据fsync持久化到磁盘上。

    因此一个事务在提交时,需要经历两次刷盘(redo log和binlog各一次),保证数据的强一致性。

    如果MySQL的TPS为2000/s时,每秒的IO刷盘次数为4000。对MySQL肯定是灾难级别的性质了,会带来严重的性能影响。

    image.png

    优化——组提交机制

    MySQL肯定不会放任这种问题存在,也是引入了组提交机制来解决,具体的流程如下:

    假设存在三个并发的事务:trx1、trx2、trx3,在二阶段提交的prepare阶段,都会将各自事务的记录写到redo log buffer里(buffer是共享的,因此记录都在同一个buffer)。在写入的时候,会带上一个LSN值,该值的计算方法为:上一个事务的LSN值 + 本次提交事务的记录长度

    image.png

    一、假设先到的trx_1是队长,后到的trx_2和trx_3是小弟。

    二、看下手表,差不多到时间了,由队长去写盘,会带着最小的trx_3的LSN值去。等到队长回来了,就表明LSN<150的数据都被持久化到磁盘去了。

    三、trx_2和trx_3的数据都被trx_1一起持久化了,因此直接返回,不用再刷盘。

    Tips: 类似TCP的累积确认机制,当一组的成员越多,越节约磁盘IO的次数。

    组提交机制的内置优化手段

    当一组的成员越多,肯定是越节约磁盘IO的次数的。因此,在队长先到redo log buffer后,后续的fsync越晚,拖得越久,所积累的小弟也就越多!

    而原先二阶段提交中的写binlog,也可以拆分成两步:write(写page cache) and fsync(写disk)

    因此,可将二阶段提交的步骤可被优化成如下:

    image.png

    原先在redo log的prepare阶段,就会直接完成fsync的步骤。为了尽可能最大化组提交的成员数量,将fsync迁移到binlog的write和fsync中间。 与此同时,binlog也变相进行了组提交(中间插入了redo log的fsync),算得上是一举多得。

    组提交机制的参数优化手段

    想更好的提高binlog的组提交效率,可以通过两个内置参数完成。如下:

    binlog_group_commit_sync_delay: 表明binlog延迟多少微秒后调用fsync,对应步骤4

    binlog_group_commit_sync_no_delay_count: 表明累计多少次后调用fsync,同样对应步骤4

    两者是或的关系,只要满足其中一个条件就会调用binlog的fsync

    二阶段提交的总结

    当MySQL碰到IO瓶颈时,有以下两种优化方法:

  • 合理设置binlog_group_commit_sync_delay和binlog_group_commit_sync_no_delay_count的值,减少binlog的IO次数。

  • 短暂修改双1策略,将sync_binlog设置大于1的值(减少binlog的持久化次数,累计N个),将innodb_flush_log_at_trx_commit设置为2,redo log只会write到page cache,不会去持久化硬盘。

  • 相关文章

    pt-kill工具的使用
    pt-ioprofile工具包的使用
    数据库管理-第216期 Oracle的高可用-01(20240703)
    DBMS_REPAIR EXAMPLE SCRIPT WITH PARTITION
    数据库事务的四大特性: ACID 
    使用BBED修复损坏的SYSTEM文件头

    发布评论