参考:

小林Coding:https://xiaolincoding.com/mysql/log/how_update.html

 ApsaraDB  : https://www.alibabacloud.com/blog/an-in-depth-analysis-of-undo-logs-in-innodb_598966#

    https://www.alibabacloud.com/blog/598965?spm=a3c0i.23458820.2359477120.4.23dc7d3f4Dt76t

《凤凰架构》一书中关于 redoLog 和 undoLog 的概念理得很清楚

https://www.cnblogs.com/suBlog/p/16592859.html

数据必须要成功写入磁盘、磁带等持久化存储器后才能拥有持久性,只存储在内存中的数据,一旦遇到应用程序忽然崩溃,或者数据库、操作系统一侧崩溃,甚至是机器突然断电宕机等情况就会丢失,后文我们将这些意外情况都统称为“崩溃”(Crash)实现原子性和持久性的最大困难是“写入磁盘”这个操作并不是原子的,不仅有“写入”与“未写入”状态,还客观存在着“正在写”的中间状态。


 

FORCE:当事务提交要求事务提交后必须同时完成写入则称为FORCE,如果不强制在事务提交后必须同时完成写入则称为NO-FORCE。现实中绝大多数数据库采用的都是NO-FORCE策略,因为只要有了日志,变动数据随时可以持久化。从优化磁盘I/O性能考虑,事务提交后并不是立刻写到磁盘上,而是 WAL (Write-Ahead Logging)先写日志,在适当的时候,再将这个操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做。

NO-FORCE 需要 redoLog重做日志 在发生崩溃时,根据 redoLog 进行重做:已经在提交了事务(redoLog会记CommitRecord)但还没有写入磁盘(redoLog没有记EndRecord)的数据


 

STEAL:在事务提交允许在事务提交前写入 则称为STEAL,不允许则称为NO-STEAL。从优化磁盘I/O性能考虑,允许数据提前写入,有利于利用空闲I/O资源,也有利于节省数据库缓存区的内存。

STEAL 需要 undoLog回滚日志事务回滚时,根据 undoLog 进行事务回滚:还没有提交事务(redoLog没有记CommitRecord,更没有EndRecord)但是已经写入了磁盘(会记undoLog)的数据


 

NO-FORCE 可以事务提交后异步写入磁盘,STEAL 可以不必等待事务提交后就开始写磁盘,这两个组合性能最快。但是一个 NO-FORCE 需要 redoLog,STEAL 需要 undoLog

 

那么以上这些概念对应到 mysql 里是什么样的呢?

  • redo log(重做日志):是 Innodb 存储引擎层生成的日志,实现了事务中的持久性主要用于掉电等故障 (crash-safe) 恢复;将数据写操作从随机写变为顺序写,提高了mysql写入磁盘的性能;
  • undo log(回滚日志):是 Innodb 存储引擎层生成的日志,实现了事务中的原子性主要用于 事务回滚  MVCC ( ReadView + undo log 版本链)。
  • binlog (归档日志):是 Server 层生成的日志,主要用于数据备份主从复制;

(事务提交前) 内存 buffer pool 中的 undoLog ----> (不管事务提交前后,只要写了 undoLog)redoLog 文件 ----> (事务提交后)binLog 文件 

redoLog: 只保证到内存缓冲区,到 pageCache 再到刷盘的时机有不同设定方式

binLog: 一定 wirte() 到 pageCache ,刷盘时机有不同设定方式

事务提交之前发生了崩溃,重启后会通过 undo log 回滚事务,事务提交之后发生了崩溃,重启后会通过 redo log 恢复事务

 

关于顺序写与随机写

写 redoLog 顺序写,写脏页随机写(修改的数据页的位置是随机的)。 顺序写比随机写效率高。

redoLog 落盘时机:

  1. MySql 正常关闭时
  2. 当 redo log buffer 中记录的写入量大于 redo log buffer 内存空间的一半时
  3. InnoDB 后台线程每隔1秒,将 redo log buffer 持久化到磁盘。

此外 redoLog 在每次提交事务时还会落盘,有三种刷盘策略:

  1. 提交事务时同步 write 和 fsync
  2. 提交事务时什么都不做,InnoDB 后台线程每隔1秒 write 和 fsync (Maybe 就是上面 3 里面那个后台线程?)
  3. 提交事务时只 write,InnoDB 后台线程每隔1秒 fsync

脏页(实际更改的记录,即聚簇索引记录页)落盘时机:

  1. redoLog 满了(因为是循环写,所以会满)
  2. Buffer Pool 满了并且淘汰的是脏页
  3. MySql 认为空闲时,后台线程定期将适量脏页刷入磁盘
  4. MySql 正常关闭前,会把所有脏页刷入磁盘

可以看到,脏页落盘频率较低,如果在脏页还没有来得及刷入到磁盘时,MySQL 宕机了,不就丢失数据了吗?

这个不用担心,InnoDB 的更新操作采用的是 Write Ahead Log 策略,即先写 redo Log 日志,再写入磁盘,通过 redo log 日志让 MySQL 拥有了崩溃恢复能力(对于那些已经提交事务但是还没刷入磁盘的数据)

MySql - undo log

不同的操作,需要记录的内容也是不同的,所以不同类型的操作(修改、删除、新增)产生的 undo log 的格式也是不同的。

每当 InnoDB 引擎提交事务前对一条记录进行操作(修改、删除、新增)时,要把回滚时需要的信息都记录到 undo log 里,比如:

    • 插入一条记录时,要把这条记录的主键值记下来,这样之后回滚时只需要把这个主键值对应的记录删掉就好了;
    • 删除一条记录时,要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录重新插入到表中就好了;
    • 更新一条记录时,要把被更新的列的旧值记下来,这样之后回滚时再把这些列更新为旧值就好了。

在发生回滚时,就读取 undo log 里的数据,然后像上面那样做相反操作。

一条记录的每一次更新操作产生的 undo log 格式都有一个 roll_pointer 指针和一个 trx_id 事务id:

    • 通过 trx_id 可以知道该记录是被哪个事务修改的;
    • 通过 roll_pointer 指针可以将这些 undo log 串成一个链表,这个链表就被称为版本链;

另外,除了事务回滚,undo log 还有一个作用,通过 ReadView + undo log 实现 MVCC(多版本并发控制)。

对于「读已提交」和「可重复读」隔离级别的事务来说,它们的快照读(普通 select 语句)是通过 Read View + undo log 来实现的,

有锁化时它们的区别是 「读已提交」没有贯穿整个事务的读锁,而「可重复读」有。对于无锁化 MVCC 实现,就在于创建新 Read View 的时机不同:

    • 「读已提交」隔离级别是在每个 select 都会生成一个新的 Read View,也意味着,事务期间的多次读取同一条数据,前后两次读的数据可能会出现不一致,因为可能这期间另外一个事务修改了该记录,并提交了事务。
    • 「可重复读」隔离级别是启动事务时生成一个 Read View然后整个事务期间都在用这个 Read View,这样就保证了在事务期间读到的数据都是事务启动前的记录。

这两个隔离级别实现是通过「事务的 Read View 里的字段」「记录中的两个隐藏列(trx_id 和 roll_pointer)」比对,如果不满足可见行,就会顺着 undo log 版本链里找到满足其可见性的记录,从而控制并发事务访问同一个记录时的行为,这就叫 MVCC(多版本并发控制)。

unLog 存储在哪里?

Innodb 存储引擎设计了一个缓冲池(Buffer Pool),来提高数据库的读写性能。

在 MySQL 启动的时候,InnoDB 会为 Buffer Pool 申请一片连续的内存空间,然后按照默认的16KB的大小划分出一个个的页, Buffer Pool 中的页就叫做缓存页。此时这些缓存页都是空闲的,之后随着程序的运行,才会有磁盘上的页被缓存到 Buffer Pool 中。

Buffer Pool 包括了缓存「索引页」和「数据页」, Undo 页,插入缓存、自适应哈希索引、锁信息等等。

undo log 会写入内存中 Buffer Pool 中的 Undo 页面。

 

 

内存修改该 Undo 页面后,需要记录对应的 redo log。

undo log 和 索引树中的数据页 的刷盘策略是一样的,都需要通过 redo log 保证持久化。

buffer pool 中有 undo 页,对 undo 页的修改也都会记录到 redo log。redo log 会每秒刷盘,提交事务时也会刷盘,数据页和 undo 页都是靠这个机制保证持久化的。

 

  

 

MySql - redo log

为了防止断电导致数据丢失的问题,当有一条记录需要更新的时候,InnoDB 引擎就会先更新内存(同时标记为脏页),然后将本次对这个页的修改以 redo log 的形式记录下来,这个时候更新就算完成了。

  • redo log 是物理日志,记录的是在某个数据页做了什么修改,比如对 XXX 表空间中的 YYY 数据页 ZZZ 偏移量的地方做了AAA 更新;

WAL (Write-Ahead Logging)技术指的是, MySQL 的写操作并不是立刻写到磁盘上,而是提交事务后先写 redoLog 日志,后续,InnoDB 引擎会在适当的时候,由后台线程再写到磁盘上。

redoLog重做日志 在发生崩溃时,根据 redoLog 进行重做:已经在提交了事务(redoLog会记CommitRecord),但还没有写入磁盘(redoLog没有记EndRecord)的数据

redo log 要写到磁盘,数据也要写磁盘,为什么要多此一举?

  • redoLog 写入磁盘顺序写,写入方式使用了追加操作,写入数据需要先找到写入位置,然后才写到磁盘。
  • 数据写入磁盘随机写

  磁盘的「顺序写 」比「随机写」 高效的多因此 redo log 写入磁盘 比 数据写入磁盘 的开销更小。

产生的 redoLog 是直接写入到磁盘的吗?

产生的 redo log 也不是直接写入磁盘的,因为这样会产生大量的 I/O 操作,而且磁盘的运行速度远慢于内存。

所以,redo log 也有自己的缓存—— redo log buffer,每当产生一条 redo log 时,会先写入到 redo log buffer,后续在持久化到磁盘如下图:

redo log buffer 默认大小 16 MB,可以通过 innodb_log_Buffer_size 参数动态的调整大小。

redo log 什么时候由 redoLog Buffer 写入到 redoLog 文件?

主要有下面几个时机:

    • MySQL 正常关闭时;
    • 当 redo log buffer 中记录的写入量大于 redo log buffer 内存空间的一半时,会触发落盘;
    • 参数为 0 和 2:InnoDB 的后台线程每隔 1 秒,将 redo log buffer 持久化到磁盘。
    • 参数为 1:每次事务提交时都将缓存在 redo log buffer 里的 redo log 直接持久化到磁盘(这个策略可由 innodb_flush_log_at_trx_commit 参数控制)

事务提交时是否写入由 redoLog Buffer 写入 redoLog 文件,由参数 innodb_flush_log_at_trx_commit 参数控制,可取的值有:0、1、2,默认值为 1,这三个值分别代表的策略如下:

    • 当设置该参数为 0 时,表示每次事务提交时 ,还是将 redo log 留在 redo log buffer 中 ,该模式下在事务提交时不会主动触发写入磁盘的操作。
      • 既不主动调 write() 也不主动调 fsync(),InnoDB 后台线程每隔一秒调 write() 和 fsycn()
      • MySQL 进程的崩溃会导致上一秒钟所有事务数据的丢失
    • 当设置该参数为 1 时,表示每次事务提交时,都将缓存在 redo log buffer 里的 redo log 直接同步持久化到磁盘,这样可以保证 MySQL 异常重启之后数据不会丢失。
      • 同步调用 write() 和 fsync()
    • 当设置该参数为 2 时,表示每次事务提交时,都将缓存在 redo log buffer 里的 redo log 调用 write() 写到操作系统的 Page Cache 里的文件(操作系统的文件系统中有个 Page Cache,Page Cache 是专门用来缓存文件数据的。此时还没有调用 fsync 真正写到磁盘
      • 同步调用 write(),InnoDB 后台线程每隔一秒调用 fsync()
      • 较取值为 0 情况下更安全,在操作系统崩溃或者系统断电的情况下,上一秒钟所有事务数据才可能丢失。

这三个参数的数据安全性和写入性能的比较如下:

    • 数据安全性:参数 1 > 参数 2 > 参数 0
    • 写入性能:参数 0 > 参数 2> 参数 1

所以,数据安全性和写入性能是熊掌不可得兼的,要不追求数据安全性,牺牲性能;要不追求性能,牺牲数据安全性。

    • 在一些对数据安全性要求比较高的场景中,显然 innodb_flush_log_at_trx_commit 参数需要设置为 1。
    • 在一些可以容忍数据库崩溃时丢失 1s 数据的场景中,我们可以将该值设置为 0,这样可以明显地减少日志同步到磁盘的 I/O 操作。
    • 安全性和性能折中的方案就是参数 2,虽然参数 2 没有参数 0 的性能高,但是数据安全性方面比参数 0 强,因为参数 2 只要操作系统不宕机,即使数据库崩溃了,也不会丢失数据,同时性能方便比参数 1 高。

redo log 文件写满了怎么办?

默认情况下, InnoDB 存储引擎有 1 个重做日志文件组( redo log Group),「重做日志文件组」由有 2 个 redo log 文件组成,这两个 redo 日志的文件名叫 : ib_logfile0 和 ib_logfile1 。

重做日志文件组是以循环写的方式工作的,从头开始写,写到末尾就又回到开头,相当于一个环形。

所以 InnoDB 存储引擎会先写 ib_logfile0 文件,当 ib_logfile0 文件被写满的时候,会切换至 ib_logfile1 文件,当 ib_logfile1 文件也被写满时,会切换回 ib_logfile0 文件。

redo log 是循环写的方式,相当于一个环形,InnoDB 用 write pos 表示 redo log 当前记录写到的位置,用 checkpoint 表示当前要擦除的位置,如下图:

 

   

图中的:

    • write pos 和 checkpoint 的移动都是顺时针方向;
    • write pos ~ checkpoint 之间的部分(图中的红色部分),可以来记录新操作的可用部分;
    • check point ~ write pos 之间的部分(图中蓝色部分):待落盘的记录,也是此时崩溃后要重做的部分;

如果 write pos 追上了 checkpoint,就意味着 redo log 文件满了,这时 MySQL 不能再执行新的更新操作,也就是说 MySQL 会被阻塞(因此所以针对并发量大的系统,适当设置 redo log 的文件大小非常重要),此时会停下来将记录刷新到磁盘中,然后标记 redo log 哪些记录可以被擦除,接着对旧的 redo log 记录进行擦除,等擦除完旧记录腾出了空间,checkpoint 就会往后移动(图中顺时针),然后 MySQL 恢复正常运行,继续执行新的更新操作。

 

 

MySql - bin log

为什么有了 undo log 还要有 bin log?

这个问题跟 MySQL 的时间线有关系。

最开始 MySQL 里并没有 InnoDB 引擎,MySQL 自带的引擎是 MyISAM,但是 MyISAM 没有 crash-safe 的能力,binlog 日志只能用于归档。

InnoDB 是另一个公司以插件形式引入 MySQL 的,既然只依靠 binlog 是没有 crash-safe 能力的,所以 InnoDB 使用 redo log 来实现 crash-safe 能力。

undo log 和 bin log 有什么区别?

这两个日志有四个区别。

1、适用对象不同:

    • binlog 是 MySQL 的 Server 层实现的日志,所有存储引擎都可以使用
    • redo log 是 Innodb 存储引擎实现的日志;

2、文件格式不同:

    • binlog 有 3 种格式类型,分别是 STATEMENT(默认格式)、ROW、 MIXED,区别如下:
      • STATEMENT:每一条修改数据的 SQL 都会被记录到 binlog 中(相当于记录了逻辑操作,所以针对这种格式, binlog 可以称为逻辑日志),主从复制中 slave 端再根据 SQL 语句重现。但 STATEMENT 有动态函数的问题,比如你用了 uuid 或者 now 这些函数,你在主库上执行的结果并不是你在从库执行的结果,这种随时在变的函数会导致复制的数据不一致;
      • ROW:记录行数据最终被修改成什么样了(这种格式的日志,就不能称为逻辑日志了),不会出现 STATEMENT 下动态函数的问题。但 ROW 的缺点是每行数据的变化结果都会被记录,比如执行批量 update 语句,更新多少行数据就会产生多少条记录,使 binlog 文件过大,而在 STATEMENT 格式下只会记录一个 update 语句而已;
      • MIXED:包含了 STATEMENT 和 ROW 模式,它会根据不同的情况自动使用 ROW 模式和 STATEMENT 模式;
    • redo log 是物理日志,记录的是在某个数据页做了什么修改,比如对 XXX 表空间中的 YYY 数据页 ZZZ 偏移量的地方做了AAA 更新;

3、写入方式不同:

    • binlog 是追加写,写满一个文件,就创建一个新的文件继续写,不会覆盖以前的日志,保存的是全量的日志。
    • redo log 是循环写,日志空间大小是固定,是会边写边擦除日志的,只记录未被刷入磁盘的数据的物理日志已经刷入磁盘的数据都会从 redo log 文件里擦除。

4、用途不同:

    • binlog 用于备份恢复、主从复制
    • redo log 用于掉电等故障恢复

如果不小心整个数据库的数据被删除了,能使用 redo log 文件恢复数据吗?

不可以使用 redo log 文件恢复,只能使用 binlog 文件恢复。

因为 redo log 文件是循环写,是会边写边擦除日志的,只记录未被刷入磁盘的数据的物理日志,已经刷入磁盘的数据都会从 redo log 文件里擦除。

binlog 文件保存的是全量的日志,也就是保存了所有数据变更的情况,理论上只要记录在 binlog 上的数据,都可以恢复,所以如果不小心整个数据库的数据被删除了,得用 binlog 文件恢复数据。

主从复制怎么实现?

MySQL 的主从复制依赖于 binlog ,也就是记录 MySQL 上的所有变化并以二进制形式保存在磁盘上。复制的过程就是将 binlog 中的数据从主库传输到从库上。

这个过程一般是异步的,也就是主库上执行事务操作的线程不会等待复制 binlog 的线程同步完成。

具体详细过程如下:

    • MySQL 主库在收到客户端提交事务的请求之后,会先写入 binlog,再提交事务,更新存储引擎中的数据,事务提交完成后,返回给客户端“操作成功”的响应
    • 从库会创建一个专门的 I/O 线程,连接主库的 log dump 线程,来接收主库的 binlog 日志,再把 binlog 信息写入 relay log 的中继日志,再返回给主库“复制成功”的响应
    • 从库会创建一个用于回放 binlog 的线程,去读 relay log 中继日志然后回放 binlog 更新存储引擎中的数据,最终实现主从的数据一致性。

 在完成主从复制之后,你就可以在写数据时只写主库,在读数据时只读从库,这样即使写请求会锁表或者锁记录,也不会影响读请求的执行。

 

从库是不是越多越好?

不是的。

因为从库数量增加,从库连接上来的 I/O 线程也比较多主库也要创建同样多的 log dump 线程来处理复制的请求,对主库资源消耗比较高,同时还受限于主库的网络带宽。

所以在实际使用中,一个主库一般跟 2~3 个从库(1 套数据库,1 主 2 从 1 备主),这就是一主多从的 MySQL 集群结构。

MySql 主从复制还有哪些模型?

主要有三种:

    • 同步复制:MySQL 主库提交事务的线程要等待所有从库的复制成功响应,才返回客户端结果。这种方式在实际项目中,基本上没法用,原因有两个:一是性能很差,因为要复制到所有节点才返回响应;二是可用性也很差,主库和所有从库任何一个数据库出问题,都会影响业务。
    • 异步复制(默认模型):MySQL 主库提交事务的线程并 不会等待各从库复制成功的响应,就返回客户端结果。这种模式一旦主库宕机,数据就会发生丢失。
    • 半同步复制:MySQL 5.7 版本之后增加的一种复制方式,介于两者之间,事务线程不用等待所有的从库复制成功响应,只要一部分复制成功响应回来就行,比如一主二从的集群,只要数据成功复制到任意一个从库上,主库的事务线程就可以返回给客户端。这种半同步复制的方式,兼顾了异步复制和同步复制的优点,即使出现主库宕机,至少还有一个从库有最新的数据,不存在数据丢失的风险。

bin log 什么时候刷盘?

事务执行过程中,先把日志写到 binlog cache(Server 层的 cache),事务提交的时候,再把 binlog cache 写到 binlog 文件中。

一个事务的 binlog 是不能被拆开的,因此无论这个事务有多大(比如有很多条语句),也要保证一次性写入。如果一个事务的 binlog 被拆开的时候,在备库执行就会被当做多个事务分段自行,这样破坏了原子性,是有问题的。

事务提交的时候,执行器把 binlog cache 里的完整事务写入到 binlog 文件中(肯定会 write() 到 pageCache,但不一定 fsync() 到了磁盘 ,并清空 binlog cache。如下图:

然每个线程有自己 binlog cache,但是最终都写到同一个 binlog 文件:

    • 图中的 write,指的就是指把日志写入到 binlog 文件,但是并没有把数据持久化到磁盘,因为数据还缓存在文件系统的 page cache 里,write 的写入速度还是比较快的,因为不涉及磁盘 I/O。
    • 图中的 fsync,才是将数据持久化到磁盘的操作,这里就会涉及磁盘 I/O,所以频繁的 fsync 会导致磁盘的 I/O 升高。

MySQL提供一个 sync_binlog 参数来控制数据库的 binlog 刷到磁盘上的频率:

    • sync_binlog = 0 的时候,表示每次提交事务都只 write,不 fsync,后续交操作系统决定何时 fsync 将数据持久化到磁盘;
    • sync_binlog = 1 的时候,表示每次提交事务都会 write,然后马上执行 fsync;
    • sync_binlog =N(N>1) 的时候,表示每次提交事务都 write,但累积 N 个事务后才 fsync。

在MySQL中系统默认的设置是 sync_binlog = 0,也就是不做任何强制性的磁盘刷新指令,这时候的性能是最好的,但是风险也是最大的。因为一旦主机发生异常重启,还没持久化到磁盘的数据就会丢失。

而当 sync_binlog 设置为 1 的时候,是最安全但是性能损耗最大的设置。因为当设置为 1 的时候,即使主机发生异常重启,最多丢失一个事务的 binlog,而已经持久化到磁盘的数据就不会有影响,不过就是对写入性能影响太大。

如果能容少量事务的 binlog 日志丢失的风险,为了提高写入的性能,一般会 sync_binlog 设置为 100~1000 中的某个数值。

 

 

update 语句执行的全过程

当优化器分析出成本最小的执行计划后,执行器就按照执行计划开始进行更新操作。

具体更新一条记录 UPDATE t_user SET name = 'xiaolin' WHERE id = 1; 的流程如下:

  1. 执行器负责具体执行,会调用存储引擎的接口,通过主键索引树搜索获取 id = 1 这一行记录:
    • 如果 id=1 这一行所在的数据页本来就在 buffer pool 中,就直接返回给执行器更新;
    • 如果记录不在 buffer pool,将数据页从磁盘读入到 buffer pool,返回记录给执行器。
  2. 执行器得到聚簇索引记录后,会看一下更新前的记录和更新后的记录是否一样:
    • 如果一样的话就不进行后续更新流程;
    • 如果不一样的话就把更新前的记录和更新后的记录都当作参数传给 InnoDB 层,让 InnoDB 真正的执行更新记录的操作;
  3. 开启事务, InnoDB 层更新记录前,首先要记录相应的 undo log,因为这是更新操作,需要把被更新的列的旧值记下来,也就是要生成一条 undo log,undo log 会写入 Buffer Pool 中的 Undo 页面,不过在内存修改该 Undo 页面后,需要记录对应的 redo log。
  4. InnoDB 层开始更新记录,会先更新内存(同时标记为脏页),然后将记录写到 redo log 里面,这个时候更新就算完成了。为了减少磁盘I/O,采用 WAL 技术,MySQL 的写操作并不是立刻写到磁盘上,而是先写 redo 日志,然后在合适的时间再将修改的行数据写到磁盘上。
  5. 至此,一条记录更新完了。
  6. 在一条更新语句执行完成后,然后开始记录该语句对应的 binlog,此时记录的 binlog 会被保存到 binlog cache,并没有刷新到硬盘上的 binlog 文件,在事务提交时才会统一将该事务运行过程中的所有 binlog 刷新到硬盘
  7. 分布式事务提交(为了方便说明,这里不说组提交的过程,只说两阶段提交):
    • prepare 阶段:将 redo log 对应的事务状态设置为 prepare,然后将 redo log 刷新到硬盘;
    • commit 阶段:将 binlog 刷新到磁盘,接着调用引擎的提交事务接口,将 redo log 状态设置为 commit(将事务设置为 commit 状态后,刷入到磁盘 redo log 文件);
  8. 至此,一条更新语句执行完成。

(事务提交前) 内存 buffer pool 中的 undoLog ----> (不管事务提交前后,只要写了 undoLog)redoLog 文件 ----> (事务提交后)binLog 文件 

redoLog: 只保证到内存缓冲区,到 pageCache 再到刷盘的时机有不同设定方式

 binLog: 一定 wirte() 到 pageCache ,刷盘时机有不同设定方式

 

 

redoLog 和 binLog 的一致性:两阶段提交

事务提交后,redo log 和 binlog 都要持久化到磁盘,但是这两个是独立的逻辑,可能出现半成功的状态,这样就造成两份日志之间的逻辑不一致。

举个例子,假设 id = 1 这行数据的字段 name 的值原本是 'jay',然后执行 UPDATE t_user SET name = 'xiaolin' WHERE id = 1; 如果在持久化 redo log 和 binlog 两个日志的过程中,出现了半成功状态,那么就有两种情况:

  • 如果在将 redo log 刷入到磁盘之后, MySQL 突然宕机了,而 binlog 还没有来得及写入。MySQL 重启后,通过 redo log 能将 Buffer Pool 中 id = 1 这行数据的 name 字段恢复到新值 xiaolin,但是 binlog 里面没有记录这条更新语句,在主从架构中,binlog 会被复制到从库,由于 binlog 丢失了这条更新语句,从库的这一行 name 字段是旧值 jay,与主库的值不一致性;
  • 如果在将 binlog 刷入到磁盘之后, MySQL 突然宕机了,而 redo log 还没有来得及写入。由于 redo log 还没写,崩溃恢复以后这个事务无效,所以 id = 1 这行数据的 name 字段还是旧值 jay,而 binlog 里面记录了这条更新语句,在主从架构中,binlog 会被复制到从库,从库执行了这条更新语句,那么这一行 name 字段是新值 xiaolin,与主库的值不一致性;

可以看到,在持久化 redo log 和 binlog 这两份日志的时候,如果出现半成功的状态,就会造成主从环境的数据不一致性。这是因为 redo log 影响主库的数据,binlog 影响从库的数据,所以 redo log 和 binlog 必须保持一致才能保证主从数据一致。

两阶段提交把单个事务的提交拆分成了 2 个阶段,分别是「准备(Prepare)阶段」和「提交(Commit)阶段」,每个阶段都由协调者(Coordinator)和参与者(Participant)共同完成。注意,不要把提交(Commit)阶段和 commit 语句混淆了,commit 语句执行的时候,会包含提交(Commit)阶段。

  • 准备阶段:协调者询问所有节点是否准备好了,节点听到后做出应答 准备好了/没准备好
  • 提交阶段:如果所有节点都回答准备好了,正式开始;如果任何一节点响应没有准备好,停止继续,对应事务中的回滚操作。

MySql 的两阶段提交

 

从图中可看出,事务的提交过程有两个阶段,就是将 redo log 的写入拆成了两个步骤:prepare 和 commit,中间再穿插写入binlog,具体如下:

  • prepare 阶段:将 XID(内部 XA 事务的 ID) 写入到 redo log,同时将 redo log 对应的事务状态设置为 prepare,然后将 redo log 持久化到磁盘(innodb_flush_log_at_trx_commit = 1 的作用);

  • commit 阶段:把 XID 写入到 binlog,然后将 binlog 持久化到磁盘(sync_binlog = 1 的作用),接着调用引擎的提交事务接口,将 redo log 状态设置为 commit,此时该状态并不需要持久化到磁盘,只需要 write 到文件系统的 page cache 中就够了,因为只要 binlog 写磁盘成功,就算 redo log 的状态还是 prepare 也没有关系,一样会被认为事务已经执行成功;

异常重启会出现什么现象?

我们来看看在两阶段提交的不同时刻,MySQL 异常重启会出现什么现象?下图中有时刻 A 和时刻 B 都有可能发生崩溃:

不管是时刻 A(redo log 已经写入磁盘, binlog 还没写入磁盘),还是时刻 B (redo log 和 binlog 都已经写入磁盘,还没写入 commit 标识)崩溃,此时的 redo log 都处于 prepare 状态。

在 MySQL 重启后会按顺序扫描 redo log 文件,碰到处于 prepare 状态的 redo log,就拿着 redo log 中的 XID 去 binlog 查看是否存在此 XID:

  • 如果 binlog 中没有当前内部 XA 事务的 XID,说明 redolog 完成刷盘,但是 binlog 还没有刷盘,则回滚事务。对应时刻 A 崩溃恢复的情况。
  • 如果 binlog 中有当前内部 XA 事务的 XID,说明 redolog 和 binlog 都已经完成了刷盘,则提交事务。对应时刻 B 崩溃恢复的情况。

可以看到,对于处于 prepare 阶段的 redo log,即可以提交事务,也可以回滚事务,这取决于是否能在 binlog 中查找到与 redo log 相同的 XID,如果有就提交事务,如果没有就回滚事务。这样就可以保证 redo log 和 binlog 这两份日志的一致性了。

所以说,两阶段提交是以 binlog 写成功为事务提交成功的标识,因为 binlog 写成功了,就意味着能在 binlog 中查找到与 redo log 相同的 XID。

两阶段提交有什么问题?

两阶段提交虽然保证了两个日志文件的数据一致性,但是性能很差,主要有两个方面的影响:

  • 磁盘 I/O 次数高:对于“双1”配置每个事务提交都会进行两次 fsync(刷盘),一次是 redo log 刷盘,另一次是 binlog 刷盘
  • 锁竞争激烈:两阶段提交虽然能够保证「单事务」两个日志的内容一致,但在「多事务」的情况下,却不能保证两者的提交顺序一致,因此,在两阶段提交的流程基础上,还需要加一个锁来保证提交的原子性,从而保证多事务的情况下,两个日志的提交顺序一致。

为什么两阶段提交的磁盘 I/O 次数会很高?

binlog 和 redo log 在内存中都对应的缓存空间,binlog 会缓存在 binlog cache,redo log 会缓存在 redo log buffer,它们持久化到磁盘的时机分别由下面这两个参数控制。一般我们为了避免日志丢失的风险,会将这两个参数设置为 1:

  • 当 sync_binlog = 1 的时候,表示每次提交事务都会将 binlog cache 里的 binlog 直接持久到磁盘;
  • 当 innodb_flush_log_at_trx_commit = 1 时,表示每次事务提交时,都将缓存在 redo log buffer 里的 redo log 直接持久化到磁盘;

可以看到,如果 sync_binlog 和 当 innodb_flush_log_at_trx_commit 都设置为 1,那么在每个事务提交过程中, 都会至少调用 2 次刷盘操作,一次是 redo log 刷盘,一次是 binlog 落盘,所以这会成为性能瓶颈。

为什么锁竞争激烈?

在早期的 MySQL 版本中,通过使用 prepare_commit_mutex 锁来保证事务提交的顺序,在一个事务获取到锁时才能进入 prepare 阶段,一直到 commit 阶段结束才能释放锁,下个事务才可以继续进行 prepare 操作。

通过加锁虽然完美地解决了顺序一致性的问题,但在并发量较大的时候,就会导致对锁的争用,性能不佳。