MySQL日志事务原理 -- undologredologbinlog两阶段提交

Posted arctic_fox

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MySQL日志事务原理 -- undologredologbinlog两阶段提交相关的知识,希望对你有一定的参考价值。

 

 

 

 

1. undolog

1.1  undolog – 原子性

 

 

 


1.2 回滚日志,记录数据被修改前的信息,属于逻辑日志

  • 什么是逻辑日志?

比如我们执行一条delete语句,undolog里边记录的是相反的操作insert记录【相当于存放的是操作逻辑语句,而不是数据】

  • 逻辑日志好处

比如全表更新,如果是物理日志,我们需要把全表的数据都存下来
若是逻辑日志,只需要存放一条语句就可以恢复了

 

1.3 undolog用处

  • 回滚

一个事务在执行过程中,在还没有提交事务之前,如果MySQL 发生了崩溃,要怎么回滚到事务之前的数据呢?

在事务没提交之前,MySQL 会先记录更新前的数据到 undo log 日志文件里面,当事务回滚时,可以利用 undo log 来进行回滚。

  • 版本链

类似一个链表,通过回滚指针,串联起来

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

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

 

  • MVCC

通过 ReadView + undo log 实现 MVCC

 

 

 

 

2. redolog – 持久性

重做日志,物理记录

redo log 是物理日志,记录内容是“在某个数据页上做了什么修改”,属于 InnoDB 存储引擎。

 

2.1 前置知识-- Buffer Pool

Buffer Pool是很经典的缓存池,其中又以Page为单位

  • 空闲页
  • 干净页
  • 脏页【类似操作系统中,修改位为1】

 

 

 

 


2.2 正常同步


首先我们要知道,每次事务提交后,首先是跟 Buffer pool 打交道

  • 若缓存中没有我们要操作的数据【类似缺页中断,MySQL 中数据是以页为单位】,则会启动后台线程,去磁盘中拉取过来
  • 之后就都是直接操作缓存了

现在缓存的内容更新好了,但磁盘的内容还是旧的,何时更新到磁盘呢?
我们可能会有如下方案:

  • 每隔一段时间,就同步到磁盘
    • 弊端

每次更新都要写入磁盘,磁盘IO很高
如果每一次的更新操作都需要写进磁盘,然后磁盘也要找到对应的那条记录,然后再更新,整个过程 IO 成本、查找成本都很高。 不如先把更新的操作,记录在redolog日志里边,之后在适当的时候,再一起刷盘!【而不是每次更新就一条一条的刷】

  • 这就是MySQL的 WAL 技术,(Write-Ahead Logging) 先写进日志,再刷入磁盘

buffer挂掉

一般情况下都没什么问题,但可能在执行某次更新操作的时候,buffer挂掉了呢?此时同步过程就GG了,导致缓存不一致

因此,为了双保险,我们还会把数据相关的变化【物理修改】存到redolog里边,用来实现事务的持久性【宕机后还能查询redolog,来进行恢复】

 

2.3 redolog保险出场


当事务提交时,会把“在某个数据页上做了什么修改”记录到重做日志缓存(redo log buffer)里,接着刷盘到 redo log 文件里。

 

 

 

 

2.3.1 缓存redolog – Log Buffer

 

 

 

 

 

 

 

 


2.3.2 刷盘时机


MySQL 正常关闭时
当 redo log buffer 中记录的写入量大于 redo log buffer 内存空间的一半时,会触发落盘
每次事务提交时【具体不同策略不一样】
一般情况下,事务一提交就会进行刷盘操作。

InnoDB 存储引擎为 redo log 的刷盘策略提供了 innodb_flush_log_at_trx_commit 参数,它支持三种策略:

  • 0 :设置为 0 的时候,表示每次事务提交时不进行刷盘操作
  • 1 :设置为 1 的时候,表示每次事务提交时都将进行刷盘操作(默认值)
  • 2 :设置为 2 的时候,表示每次事务提交时都只把 redo log buffer 内容写入 page cache

 

 

 

 


后台线程
InnoDB 存储引擎有一个后台线程,每隔1 秒,就会把 redo log buffer 中的内容写到文件系统缓存(page cache),然后调用 fsync 刷盘。

 

 

 

不同参数对应情况
innodb_flush_log_at_trx_commit 设置为不同值时,分别是什么时候将 redo log 写入磁盘?

参数0:事务提交时不刷盘,而是靠后台线程,把缓存在 redo log buffer 中的 redo log ,通过调用 write() 写到操作系统的 Page Cache,然后调用 fsync() 持久化到磁盘。所以参数为 0 的策略,MySQL 进程的崩溃会导致上一秒钟所有事务数据的丢失;
参数1:只要事务提交成功,redo log记录就一定在硬盘里,不会有任何数据丢失。
如果事务执行期间MySQL挂了或宕机,这部分日志丢了,但是事务并没有提交,所以日志丢了也不会有损失!!

参数2:只要事务提交成功,redo log buffer中的内容都会写入文件系统缓存(page cache)。调用 fsync,将缓存在**操作系统中 Page Cache **里的 redo log 持久化到磁盘。
如果仅仅只是MySQL挂了不会有任何数据丢失,只有操作系统崩溃的情况下,上一秒钟所有事务数据才可能丢失。

如何选择参数

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

在一些对数据安全性要求比较高的场景中,显然 innodb_flush_log_at_trx_commit 参数需要设置为 1。

在一些可以容忍数据库崩溃时丢失 1s 数据的场景中,我们可以将该值设置为 0,这样可以明显地减少日志同步到磁盘的 I/O 操作。

安全性和性能折中的方案就是参数 2,虽然参数 2 没有参数 0 的性能高,但是数据安全性方面比参数 0 强,因为参数 2 只要操作系统不宕机,即使数据库崩溃了,也不会丢失数据,同时性能方便比参数 1 高。

 

2.3.4 好处
redolog是文件追加的形式,而缓存同步到磁盘是随机IO
可以说这是 WAL 技术的另外一个优点:MySQL 的写操作从磁盘的「随机写」变成了「顺序写」

2.3.4 总结
redolog的用处:

  • 实现事务的持久性
  • 将写操作从磁盘的「随机写」变成了「顺序写」

 

2.4 redolog日志文件组 – 应对写满的情况


硬盘上存储的 redo log 日志文件不止一个,而是以一个日志文件组的形式出现的,每个的redo日志文件大小都是一样的

比如可以配置为一组4个文件,每个文件的大小是 1GB,整个 redo log 日志文件组可以记录4G的内容。 它采用的是环形数组形式,从头开始写,写到末尾又回到头循环写,如下图所示。

 

 

 

除了写入,我们还需要擦除【redolog刷盘到磁盘后,就可以进行擦除了】,因此我们用两个指针来表示:

  • write pos:当前记录写到的位置
  • checkpoint:当前要擦除的位置

跟循环队列的设计有些神似,维护两个指针,head跟tail一般

这两个指针把整个环形划成了几部分

  • write pos - checkpoint:待写入的部分
  • checkpoint - write pos:还未刷入磁盘的记录

若write pos追上了checkpoint,说明没有空间写入了【跟我们队列满了是一样的情况】,这时候不能再写入新的 redo log 记录,MySQL 得停下来,清空一些记录,把 checkpoint 推进一下。

 

3. binlog


redo log 它是物理日志,记录内容是“在某个数据页上做了什么修改”,属于 InnoDB 存储引擎。 而 binlog 是逻辑日志,记录内容是语句的原始逻辑,类似于“给 ID=2 这一行的 c 字段加 1”,属于MySQL Server 层。 不管用什么存储引擎,只要发生了表数据更新,都会产生 binlog 日志。

 

3.1 与redolog区别


这两个日志有四个区别

 

3.1.1、适用对象不同:


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

3.1.2、文件格式不同:


binlog 有 3 种格式类型,分别是 STATEMENT(默认格式)、ROW、 MIXED,区别如下:

STATEMENT:每一条修改数据的 SQL 都会被记录到 binlog 中(相当于记录了逻辑操作,所以针对这种格式, binlog 可以称为逻辑日志),主从复制中 slave 端再根据 SQL 语句重现。
但 STATEMENT 有动态函数的问题

比如:

update_time=now()//这里会获取当前系统时间

直接执行会导致与原库的数据不一致,因此我们引入了row

ROW:记录行数据最终被修改成什么样了(这种格式的日志,就不能称为逻辑日志了),不会出现 STATEMENT 下动态函数的问题。
但也有缺点

每行数据的变化结果都会被记录,比如:

update user set name =\'melo\';

更新多少行数据就会产生多少条记录,使 binlog 文件过大,而在 STATEMENT 格式下只会记录一个 update 语句而已;

MIXED:这是目前MySQL默认的日志格式,即混合了STATEMENT 和 ROW两种格式。默认情况下采用STATEMENT,但是在一些特殊情况下采用ROW来进行记录。MIXED 格式能尽量利用两种模式的优点,而避开他们的缺点。
MySQL会判断这条SQL语句是否可能引起数据不一致,如果是,就用row格式,否则就用statement格式。

redo log 是物理日志,记录的是在某个数据页做了什么修改。

3.1.3、写入方式不同:

 

  • binlog 是追加写,写满一个文件,就创建一个新的文件继续写,不会覆盖以前的日志,保存的是全量的日志。
  • redo log 是循环写,日志空间大小是固定的,全部写满就从头开始,保存未被刷入磁盘的脏页日志。

3.1.4、用途不同:

 

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

如果不小心整个数据库的数据被删除了,能使用 redo log 文件恢复数据吗? 不可以使用 redo log 文件恢复,只能使用 binlog 文件恢复。 因为 redo log 文件是循环写,是会边写边擦除日志的,只记录未被刷入磁盘的数据的物理日志,已经刷入磁盘的数据都会从 redo log 文件里擦除。

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

 

3.2 写入时机


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

因为一个事务的binlog不能被拆开,无论这个事务多大,也要确保一次性写入,所以系统会给每个线程分配一个块内存作为binlog cache。

参数 binlog_cache_size 用于控制单个线程内 binlog cache 所占内存的大小。如果超过了这个参数规定的大小,就要暂存到磁盘(Swap)。

  

  • 什么时候 binlog cache 会写到 binlog 文件?

在事务提交的时候,执行器把 binlog cache 里的完整事务写入到 binlog 文件中,并清空 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,后续交由操作系统决定何时将数据持久化到磁盘;
  • 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 中的某个数值。

 

4. 两阶段提交

4.1 为何需要两阶段提交


主要是因为:redolog影响的是主库,而binlog涉及主从复制,影响的是从库??

也有说用binlog来作为备份恢复,恢复后的结果可能会跟主库不一样

 

执行一条更新语句时,此时宕机了,有两种情况:

  • redolog刷盘成功,但binlog没有

主库由于有redolog的存在,能够恢复,而binlog中并没有相关的更新语句,导致从库中丢失了本次更新

  • binlog刷盘成功,但redolog没有

从库由于有binlog的存在,记录了更新,binlog 会被复制到从库,从库执行了这条更新语句,而主库的redolog还没刷盘成功,导致崩溃后没法恢复,主库丢失了本次更新

 

4.2 XA事务、两阶段提交的具体流程


4.2.1 第一阶段


这里是事务管理器,相当于一个总的管理者,其负责管理参与事务的多个部分【比如这里的redolog,binlog】,放到分布式系统里边,可能是两个不同的系统

应用程序调用了事务管理器的提交方法,此后第一阶段分为两个步骤:

 

 

 

事务管理器通知参与该事务的各个资源管理器,通知他们开始准备事务。

资源管理器接收到消息后开始准备阶段,写好事务日志并执行事务,但不提交,然后将是否就绪的消息返回给事务管理器(此时已经将事务的大部分事情做完,以后的内容耗时极小)。

 

4.2.2  第二阶段
第二阶段也分为两个步骤:

 

 

 


事务管理器在接受各个消息后,开始分析,如果有任意其一失败,则发送回滚命令,否则发送提交命令。 各个资源管理器接收到命令后,执行(耗时很少),并将提交消息返回给事务管理器。

 

4.3 内部XA事务


内部XA事务主要指单节点实例内部,一个事务跨多个存储引擎进行读写,那么就会产生内部XA事务

存储引擎与插件之间,存储引擎与存储引擎之间

 

4.4 提交过程

 

将 redo log 的写入拆成了两个步骤:prepare 和 commit,中间再穿插写入binlog,具体如下:

  • prepare 阶段:将 XID(内部 XA 事务的 ID) 写入到 redo log,同时将 redo log 对应的事务状态设置为 prepare,然后将 redo log 刷新到硬盘;
  • commit 阶段:把 XID 写入到 binlog,然后将 binlog 刷新到磁盘,接着调用引擎的提交事务接口,将 调用引擎的提交事务接口,将 redo log 状态设置为 commit(将事务设置为 commit 状态后,刷入到磁盘 redo log 文件,所以 commit 状态也是会刷盘的);

4.5 出现异常会怎么样?

 

4.5.1 总结

只要binlog也好了,就不需要回滚,binlog没好,就需要回滚

其实就是两个变成一个事务,redolog和binlog

 

4.5.2 写入binlog出现异常


使用两阶段提交后,写入binlog时发生异常也不会有影响,因为MySQL根据redo log日志恢复数据时,发现redo log还处于prepare阶段,并且没有对应binlog日志【binlog没有当前内部XA事务的XID】,就会回滚该事务。

 

 

 

 

4.5.3设置commit异常


并不会回滚事务,它会执行上图框住的逻辑,虽然redo log是处于prepare阶段,但是能通过事务id找到对应的binlog日志【binlog有当前内部XA事务的XID】,所以MySQL认为是完整的,就会提交事务恢复数据。

 

 

 

4.5.4 疑问–好问题

 

在执行更新语句过程,会记录redo log与binlog两块日志,以基本的事务为单位,redo log在事务执行过程中可以不断写入,而binlog只有在提交事务时才写入,所以redo log与binlog的写入时机不一样。

  • 事务执行中间过程的 redo log 也是直接写在 redo log buffer 中的,这些缓存在 redo log buffer 里的 redo log 也会被「后台线程」每隔一秒一起持久化到磁盘。
  • 也就是说,事务没提交的时候,redo log 也是可能被持久化到磁盘的。

那事务执行过程中不断写入,如果执行事务期间mysql宕机呢? 此时重启后,mysql要利用redolog恢复,有一部分操作redolog已经写入磁盘了,后续恢复会不会出现数据不一致的情况?【因为事务实际上还没有提交呢,是需要回滚的相当于出现了异常】

此时由于有两阶段提交,binlog必须在事务提交后才写入,所以此时即使redolog写入了,但binlog还未写入,重启时读取到该redolog会发现没有对应的binlog,会放弃掉

 

5. 手撕面答环节 


5.1 MySQL 日志文件有哪些?分别介绍下作用?


分别有 undolog、redolog、 binlog。

  • undolog用于实现原子性,记录版本链用于回滚,并且属于逻辑日志,记录操作便于恢复
  • redolog用于宕机后的数据恢复,记录实际的数据,属于物理日志
  • binlog主要用于主从复制,全量备份,相比redolog可存储的内容更多,既可以物理也可以逻辑存储,一般使用Mix

其中server层还有其他的日志,比如:

  • 错误日志(error log):错误日志文件对 MySQL 的启动、运行、关闭过程进行了记录,能帮助定位 MySQL 问题。
  • 慢查询日志(slow query log):慢查询日志是用来记录执行时间超过 long_query_time 这个变量定义的时长的查询语句。通过慢查询日志,可以查找出哪些查询语句的执行效率很低,以便进行优化。
  • 一般查询日志(general log):一般查询日志记录了所有对 MySQL 数据库请求的信息,无论请求是否正确执行。

5.2 redolog如何刷入磁盘的


有三种策略,参数不同的时候,对应的刷入时机不同 首先都会先写到redolog的 buffer 区

  • 参数为0:不写入,等待后台线程定时【每秒】将 buffer 区中的内容写入磁盘

若崩溃,会丢失上一秒中,buffer区里边所有的内容

  • 参数为1:每次提交就写入,先写到 buffer 区,然后立马调用 fsync 写入磁盘

不会丢失,可以看成事务提交和写入磁盘是一个原子性的操作?

  • 参数为2:每次提交,先写到 buffer 区,然后写入到 pageCache 里边,由操作系统来决定何时写入磁盘

只有操作系统崩溃了,才会丢失上一秒中

要选择哪种策略呢?
考虑性能:0>2>1 考虑安全性:1>2>0 综合:2

 

5.3为何要引入redolog来记录呢?


将磁盘的随机IO转换为追加写入
原本如果我们每次都去更改磁盘里边的内容,则需要先随机IO找到,然后更改 有了写入的话,我们每次都先追加写入到redolog,然后再一次性去磁盘里边找就好了,而不是每次都去磁盘找

 

5.4 redolog 和 binlog 有什么区别

 

  • 存储的容量不同
    • redolog以日志文件组作为存储的数据结构,容量是有限的,写满的时候会删除 undolog以追加文件的形式存储,理论上存储容量是无限的
  • MySQL事务原理

    原子性、稳定性和持久性实现原理

      原子性、稳定性和持久性是通过redo 和 undo 日志文件实现的,不管是redo还是undo文件都会有一个缓存我们称之为redo_buf和undo_buf。同样,数据库文件也会有缓存称之为data_buf。

    4.1 undo 日志文件

      undo记录了数据在事务开始之前的值,当事务执行失败或者ROLLBACK时可以通过undo记录的值来恢复数据。例如 AA和BB的初始值分别为3,5。

    A 事务开始
    B 记录AA=3到undo_buf
    C 修改AA=1
    D 记录BB=5到undo_buf
    E 修改BB=7
    F 将undo_buf写到undo(磁盘)
    G 将data_buf写到datafile(磁盘)
    H 事务提交
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

      通过undo可以保证原子性、稳定性和持久性 
      如果事务在F之前崩溃由于数据还没写入磁盘,所以数据不会被破坏。 
      如果事务在G之前崩溃或者回滚则可以根据undo恢复到初始状态。 
      数据在任务提交之前写到磁盘保证了持久性。 
      但是单纯使用undo保证原子性和持久性需要在事务提交之前将数据写到磁盘,浪费大量I/O。

    4.2 redo/undo 日志文件

      引入redo日志记录数据修改后的值,可以避免数据在事务提交之前必须写入到磁盘的需求,减少I/O。

    A 事务开始
    B 记录AA=3到undo_buf
    C 修改AA=1 记录redo_buf
    D 记录BB=5到undo_buf
    E 修改BB=7 记录redo_buf
    F 将redo_buf写到redo(磁盘)
    G 事务提交
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

      通过undo保证事务的原子性,redo保证持久性。 
      F之前崩溃由于所有数据都在内存,恢复后重新冲磁盘载入之前的数据,数据没有被破坏。 
      FG之间的崩溃可以使用redo来恢复。 
      G之前的回滚都可以使用undo来完成。

    5 事务操作命令

      如果需要使用事务就必须选用支持事务的数据库引擎如InnoDB和Falcon,MyISAM并不支持事务。 
      在默认情况下MySQL开启的是autocommit模式,也就是隐含的将每条语句当做一个事务处理,每条SQL都会被自动提交。当我们使用BEGIN或者START TRANSCATION时会把自动提交挂起,直到显示的调用COMMIT。使用事务可以有如下两种方法:

    BEGIN; //开始事务,挂起自动提交
    insert into t_cart_shopcart (user_id, sku_id, amount, shop_id,  status) values(10001, 10001, 1, 10001, 0);
    insert into t_cart_shopcart (user_id, sku_id, amount, shop_id,  status) values(10001, 10002, 1, 10001, 0);
    COMMIT; //提交事务,恢复自动提交
    • 1
    • 2
    • 3
    • 4
    set autocommit = 0; //挂起自动提交
    insert into t_cart_shopcart (user_id, sku_id, amount, shop_id,  status) values(10001, 10001, 1, 10001, 0);
    insert into t_cart_shopcart (user_id, sku_id, amount, shop_id,  status) values(10001, 10002, 1, 10001, 0);
    COMMIT; //提交事务
    set autocommit = 1; //恢复自动提交
    • 1
    • 2
    • 3
    • 4
    • 5

      这两种方式效果相同









    以上是关于MySQL日志事务原理 -- undologredologbinlog两阶段提交的主要内容,如果未能解决你的问题,请参考以下文章

    Mysql原理篇之undo日志--上--11

    MySQL日志事务原理 -- undologredologbinlog两阶段提交

    mysql 日志--事务日志

    mysql复制的工作原理及主从复制的实现

    MySQL事务原理

    MySQL事务原理