MySql 日志详解

2022/3/20 19:29:39

本文主要是介绍MySql 日志详解,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

一、日志介绍

  MySql 日志共有错误日志、查询日志等等几个大类的日志,其中比较重要的主要是三种日志:二进制日志 binlog(归档日志)、事务日志 redolog(重做日志)、undolog(回滚日志)。每种日志的作用不尽相同,下文将对这三种日志进行详细介绍。

二、 redo日志

  我们可以想象一个场景,我们知道我们对数据库进行增删改的时候,其实是需要读取磁盘中的页,然后对页进行修改后,修改后将页写回磁盘,但单纯的以这种方式会造成下面两个问题:

1、刷新一个完成的数据页太过浪费,有时候我们只需要修改其中的一个字节,但是由于 InnoDB 是以页为单位进行磁盘 I/O 的,也就是说在该事务提交的时候不得不将一个完整的页面从内存中刷新到磁盘,我们又知道,一个页的大小是 16 K,如果修改一个字节就要刷新 16K 的数据到磁盘上,显然太浪费

2、随机 I/O 刷新起来十分慢,一个事务可能涉及到多个不同的页面,这些页面可能不相邻,这就意味着 Buffer Pool 可能会进行很多随机 I/O。

 

如何解决这两个问题?

   我们的目的是想让已经提交的事务对数据库的修改能永久的生效,即使后来系统崩溃,在重启后也能把这种修改恢复过来,所以其实没有必要在每次事务提交的时候就把所有修改的全部页面刷进磁盘,只需要把这个修改的内容记录一下就好。

  比如:“将第 0 号表空间的 100 页号中偏移量为 1000 处的值更新为 2”

  这样在事务提交时,就会把上述内容刷进磁盘,这样即使系统崩溃,重启的时候更新一下数据页,这样数据库的修改就能恢复过来,即保证了数据库的 “持久性” —— 说过的话一定要做到。这样的日志称为 redo 日志。这样做的好处:

1、日志的存储空间非常小,在存储表空间 ID 、页号、偏移量以及需要更新的值,

2、redo 日志是按顺序写入磁盘的,在执行事务过程中,每执行一条语句,就可能产生若干条 redo 日志,这些日志是按照顺序写入磁盘,也就是使用顺序 I/O

 

再思考一个问题:我们知道我们修改表会有很多种情况,比如修改索引,分页等等,如果把这些一步一步的操作直接写下来会非常大,那如何解决?

  这里 InnoDB的解决办法就是设定很多种类的日志,我们在遇见不同种类的操作的时候,只需记得其参数(如页号、偏移量等),在需要执行的时候,识别不同的日志,调用不用的函数进行对应操作,而不是记录具体操作步骤。

 

以组的形式的 redo 日志

  InnoDB中日志被划分成许多不可分割的组,如:

  向聚簇索引对应的 B+ 树的页面插入一条记录产生的 redo 是一组,是不可分割的

  向二级索引对应的 B+ 树插入一条记录产生的 redo 是一组,不可分割

 

  不可分割的指:日志的操作必须是原子操作,我们在完成一项任务的时候,要就不做,要就全部做完,所以在每一个不可分割的块中,都有开始和结束的标志,一旦开始执行,就需要执行到结束标志,否则丢弃前面的执行。

  而一个不可分割的过程称为一个 Mini-Transaction(MTR),一个MTR包含若干个 redo 日志。

 

 

redo 日志的写入过程

  • 日志的存储:为了更好的管理 redo 日志,InnoDB设计出与页相似的结构 —— redo log block, 一块 512k 的存储空间,把MTR全部都存储在了这种小的存储空间里

 

那日志直接是写入磁盘嘛? 并不是,redo 日志也有自己对应的日志缓冲区。

在服务器启动的时候就向操作系统申请了一大块称为 redo log buffer 的连续内存空间,这片空间被划分为若干个 redo log block

 

因此日志不是直接就写入磁盘,而是先写入磁盘缓冲区,那什么时候将缓冲区的日子刷入磁盘呢?有以下几种情况

1、log buffer空间不足的时候,当前 log buffer占用 50% 以上的时候,就会进行刷盘,将日志刷进磁盘

2、事务提交的时候:之所以提出 redo 日志的原因,就是其占用的空间小,而且可以顺序的写入磁盘,引入 redo 日志后,虽然在事务提交的时候可以不把修改过的 buffer pool 页面立即刷入磁盘,但为了保证持久性,必须把页面修改时对应的 redo 日志刷新进磁盘。

3、将某个脏页刷进磁盘前,必须保证对应的 redo 日志已经刷进磁盘(由于 redo 是顺序刷新的,所以在把脏页的 redo 的刷进磁盘的前提就是,前面的日志也全部刷近磁盘)

4、后台有个线程,大约每一秒会将 log buffer 中 redo 日志刷进磁盘

5、正常关闭服务器

6、做 checkpoint 时

 

什么是 checkpoint ?

  由于我们的日志文件组的容量少有限的,所以不得不循环使用 redo 日志文件组中的文件,但这会导致最后写入的日志 和 最先写入的 日志 冲突,所以 InnoDB 使用标志标记现在已经刷到哪了,另一个标志去标记新的日志写在哪,若即将发生冲突,则进行刷盘,防止覆盖,这个过程称为 checkpoint

 

其实 MySql 也提供了参数让我们去选择刷盘的策略:

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

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

innodb_flush_log_at_trx_commit 参数默认为 1 ,也就是说当事务提交时会调用 fsync 对 redo log 进行刷盘

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

 

而数据库会在合适的时候将 redo 日志更新的内容刷新进磁盘,这通常是在数据库空闲的时候进行。

 

下面是 三种参数的刷盘策略

 

 

 

 

 二、binlog

  刚刚介绍的 redo 日志是在 InnoDB 引擎上的日志,而 binlog 则是在 Server 层进行操作的日志:

  既然是在 Server 层的日志,没有区别引擎,说明所有引擎的数据库都有 binlog 日志,那为什么 InnoDB 有一套日志,MySql本身也有一套日志呢?  

  因为以前的MySQL没有InnoDB引擎,MySQL5.5前使用的 MyISAM引擎,但是 MyISAM 没有 crash-safe 的能力,而 binlog 只能用于归档。InnoDB 是后来作为 MySQL 的引擎以插件形式引入的。既然只靠 binlog 无法实现 crash-safe 的能力,所以 InnoDB 使用另一套日志系统——redo log 来实现。   那我们利用一条 updata 语句的执行过程,来理解这两个日志分别行使的作用:假如我们要将 A 数据进行 +1 操作:
  1. 我们先查到对应数据 A 的主键 ID = 2,通过主键索引树来找到这条数据所在的页号。然后在 Buffer Pool 中进行查询,如果查到,则直接操作,如果查不到,则从磁盘内进行读取。
  2. 执行器拿到引擎给的这条数据,把它 +1 后,再调用引擎接口写入这条数据。
  3. 引擎将这条数据更新至内存,同时记录到 redo log,此时 redo log 处于 prepare 态,这就告诉执行器更新已经完成,随时可以提交事务。
  4. 执行器生成这个操作的 binlog ,并把 binlog 写入磁盘
  5. 执行器调用引擎的事务接口,引擎把刚刚的 redo log 改为提交状态,这个事务执行完成  如下图所示:

 

 

  为什么 redo log有两种状态(prepare、commit)?即两阶段提交?

  为什么要有两阶段提交,就是为了让 redo log 和 binlog 两个文件保持一致。我们还是用反证法来说明,假设没有两阶段提交会发生什么问题:

  • 先写 redo log,再写 binlog,假设 redo log 写完,binlog 还没写完,MySQL 进程异常重启,redo log 写完后,即使系统崩溃,仍然能把数据恢复回来,所有恢复后的数据是正确的。但是 binlog 没写完,这时候 binlog 中就没有记录这条语句的操作,因此,之后备份日志的时候,binlog 就没有这条操作记录,如果用这个 binlog 来恢复临时库的话,由于这条语句记录的丢失,临时库就会少了这一个语句的操作,恢复出来的数据就与原库的值不同。
  • 先写 binlog,再写 redo log,如果在 binlog 写完后系统崩溃了,由于 redo log 还没写,崩溃后这个事务无效,所以磁盘数据文件中的数据是没有这条语句的操作的,但是 binlog 中已经做了记录,所以以后用这个 binlog 来做数据恢复时,就多了一个事务操作,与原库的数据不一致。

如果没有“两阶段提交”,会导致 redo log 和 binlog 记录的操作不一致,那么数据库的状态就有可能和用它的日志恢复出来的库数据不一致。

  所以,能够保证 redo log 和 binlog 的操作记录一致的流程是,将操作先更新到内存,再写入 redo log,此时标记为 prepare 状态,再写入 binlog,此时再提交事务,将 redo log 标记为 commit 状态。

 

那 binlog 的作用是什么?如图:

  

  

  binlog在MySQL的server层产生,不属于任何引擎,主要记录用户对数据库操作的SQL语句(除了查询语句)。之所以将binlog称为归档日志,是因为binlog不会像redo log一样擦掉之前的记录循环写,而是一直记录(超过有效期才会被清理),如果超过单日志的最大值(默认1G,可以通过变量 max_binlog_size 设置),则会新起一个文件继续记录。但由于日志可能是基于事务来记录的(如InnoDB表类型),而事务是绝对不可能也不应该跨文件记录的,如果正好binlog日志文件达到了最大值但事务还没有提交则不会切换新的文件记录,而是继续增大日志,所以 max_binlog_size 指定的值和实际的binlog日志大小不一定相等。

正是由于binlog有归档的作用,所以binlog主要用作主从同步和数据库基于时间点的还原。

那么回到刚才的问题,binlog可以简化掉吗?这里需要分场景来看:

  1. 如果是主从模式下,binlog是必须的,因为从库的数据同步依赖的就是binlog;
  2. 如果是单机模式,并且不考虑数据库基于时间点的还原,binlog就不是必须,因为有redo log就可以保证crash-safe能力了;但如果万一需要回滚到某个时间点的状态,这时候就无能为力,所以建议binlog还是一直开启;

 

binlog 的写入机制:binlog 同样是先写入到 binlog cache,事务提交的时候再把 binlog cache 的内容写入磁盘,如果内存不足的时候可以利用 磁盘来进行 swap 存储 。

 

 

binlog 和 redolog :

  • redo log 是InnoDB 引擎特有的;而 binlog 是MySQL Server 层实现的
  • redo log 是物理日志,记录的是“在某个数据页做了什么修改”;而 binlog 是逻辑日志,记录的是语句的原始逻辑。比如 update T set c=c+1 where ID=2;这条SQL,redo log 中记录的是 :xx页号,xx偏移量的数据修改为xxx;binlog 中记录的是:id = 2 这一行的 c 字段 +1
  • redo log 是循环写的,固定空间会用完;binlog 可以追加写入,一个文件写满了会切换到下一个文件写,并不会覆盖之前的记录
  • 记录内容时间不同,redo log 记录事务发起后的 DML 和 DDL语句;binlog 记录commit 完成后的 DML 语句和 DDL 语句
  • 作用不同,redo log 作为异常宕机或者介质故障后的数据恢复使用;binlog 作为恢复数据使用,主从复制搭建。

 

 

 讲到这里,我们来想一个完整的问题,如果 MySQL 在提交事务的时候突然崩溃,重启的时候数据是如何恢复的?

   下面就是 MySQL 在重启恢复后,在提供服务前需要做的事情:

 

 

 简单来说共有两步:

1、检查已经刷盘的 redo 日志是否已经更新进入数据库,即日志是否和数据一致

2、保证 redo log 和 binlog一致,奔溃重启后会检查redo log中是完整并且处于prepare状态的事务,然后根据XID(事务ID),从binlog中找到对应的事务,如果找不到,则回滚,找到并且事务完整则重新commit redo log,完成事务的提交。

 

那这里我们崩溃的时刻有几种情况:

    1. 时刻A(刚在内存中更改完数据页,还没有开始写redo log的时候奔溃):
      因为内存中的脏页还没刷盘,也没有写redo log和binlog,即这个事务还没有开始提交,所以奔溃恢复跟该事务没有关系;
    2. 时刻B(正在写redo log或者已经写完redo log并且落盘后,处于prepare状态,还没有开始写binlog的时候奔溃):
      恢复后会判断redo log的事务是不是完整的,如果不是则根据undo log回滚;如果是完整的并且是prepare状态,则进一步判断对应的事务binlog是不是完整的,如果不完整则一样根据undo log进行回滚;
    3. 时刻C(正在写binlog或者已经写完binlog并且落盘了,还没有开始commit redo log的时候奔溃):
      恢复后会跟时刻B一样,先检查redo log中是完整并且处于prepare状态的事务,然后判断对应的事务binlog是不是完整的,如果不完整则一样根据undo log回滚,完整则重新commit redo log;
    4. 时刻D(正在commit redo log或者事务已经提交完的时候,还没有反馈成功给客户端的时候奔溃):
      恢复后跟时刻C基本一样,都会对照redo log和binlog的事务完整性,来确认是回滚还是重新提交。

参考 : https://zhuanlan.zhihu.com/p/142491549

 

那这里涉及到的 undo 日志就是下面我要介绍的。

 

三、undo log

  前面说到用 redo 和 binlog 保证服务器崩溃的时候数据的恢复,这样可以保证已提交事务的持久性,但前面介绍过一种情况,在事务还没有提交的时候,写的 redo 日志很可能已经刷盘,那么这些未提交事务的修改过的页在 MySql 重启的时候可能也被恢复了,但为了保证事务的原子性 —— 要么不做,要么全部做完,这里就可能会出现需要回滚数据的任务,这就要用到 undo 日志 —— 做过的事情想反悔。

  undo 日志记载了回滚一个操作所需的必要内容。而其原理设计到一个概念 : 事务 ID

  

事务 ID:

  在事务对表中记录进行改动的时候(如 UpDate),才会为这个事务分配一个唯一的事务 ID,这个事务 ID 是一个递增的数字,未被分配事务 ID 的事务默认为 0, 聚簇索引记录中有一个 trx_id 隐藏列它代表对这个聚簇索引记录进行改动的语句所在的事务对应的事务 ID。(在 undo 在 mvcc 中的应用中会用到)

 

 InnoDB 利用专门的页面去存储 undo 日志,而在记录中有着专门指向 undo 记录的指针,如下图所示:

 

 

 当需要回滚的时候,就会调用之前的 undo 记录,进行数据的还原。

 

 

 

 

 



这篇关于MySql 日志详解的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程