单点突破:MySQL之日志
阅读原文时间:2023年07月08日阅读:3

前言

开发环境:MySQL5.7.31

日志是 mysql 数据库的重要组成部分,记录着数据库运行期间各种状态信息。若数据库发生故障,可通过不同日志记录恢复数据库的原来数据。因此实际上日志系统直接决定着MySQL运行的鲁棒性和稳健性。MySQL中有六种日志文件,分别是:重做日志(redo log)、回滚日志(undo log)、二进制日志(binlog)、错误日志(errorlog)、慢查询日志(slow query log)、一般查询日志(general log),中继日志(relay log)。

其中开发中比较关注的是重做日志(redo log)、回滚日志(undo log)、二进制日志(binlog),它们也是面试中关于mysql日志的常客。

日志需要有事务的基础,如果有想要了解事务,欢迎访问我的另一篇博客:

重做日志(redo log)

重做日志(redo log)是InnoDB引擎层的日志,用来记录事务操作引起数据的变化,记录的是数据页的物理修改。作用是:确保事务的持久性。防止在发生故障的时间点,尚有脏页未写入磁盘,在重启mysql服务的时候,根据redo log进行重做,从而达到事务的持久性这一特性。

在 MySQL 中,如果每一次的更新操作都需要写进磁盘,然后磁盘也要找到对应的那条记录,然后再更新,整个过程 IO 成本、查找成本都很高。为了解决这个问题,MySQL 的设计者就采用了日志(redo log)来提升更新效率。

而日志和磁盘配合的整个过程,其实就是 MySQL 里的 WAL 技术,WAL 的全称是 Write-Ahead Logging,这就是所谓的预写式技术,它的关键点就是先写日志,再写磁盘这种技术可以大大减少IO操作的频率,提升数据刷新的效率。

具体来说,当有一条记录需要更新的时候,InnoDB 引擎就会先把记录写到 redo log(redolog buffer)里面,并更新内存(buffer pool),这个时候更新就算完成了。同时,InnoDB 引擎会在适当的时候(如系统空闲时),将这个操作记录更新到磁盘里面(刷脏页)。

我们都知道,事务的四大特性里面有一个是持久性,具体来说就是只要事务提交成功,那么对数据库做的修改就被永久保存下来了,不可能因为任何原因再回到原来的状态。

那么mysql是如何保证一致性的呢?

最简单的做法是在每次事务提交的时候,将该事务涉及修改的数据页全部刷新到磁盘中。但是这么做会有严重的性能问题,主要体现在两个方面:

  • 因为 Innodb 是以为单位进行磁盘交互的,而一个事务很可能只修改一个数据页里面的几个字节,这个时候将完整的数据页刷到磁盘的话,太浪费资源了!
  • 一个事务可能涉及修改多个数据页,并且这些数据页在物理上并不连续,使用随机IO写入性能太差!

因此 mysql 设计了 redo log , 具体来说就是只记录事务对数据页做了哪些修改,这样就能完美地解决性能问题了(相对而言文件更小并且是顺序IO)。

redo log 包括两部分:一个是内存中的日志缓冲( redo log buffer),另一个是磁盘上的日志文件(redo logfile)。

mysql 每执行一条 DML 语句,先将记录写入 redo log buffer,后续某个时间点再一次性将多个操作记录写到 redo log file。这种技术就是上文提到的 WAL(Write-Ahead Logging) 技术。

在计算机操作系统中,用户空间( user space )下的缓冲区数据一般情况下是无法直接写入磁盘的,中间必须经过操作系统内核空间( kernel space )缓冲区( OS Buffer )。因此, redo log buffer 写入 redo logfile 实际上是先写入 OS Buffer ,然后再通过系统调用 fsync() 将其刷到 redo log file中,具体如下图:

.jpg)

看起来很复杂,但只要记住redo log是先写日志再写磁盘就行,这些只是redo log的执行过程,而且上述的过程更接近脏日志刷盘,如果看不明白上面的知识,我们下面从两个方面展开说明redo log

脏日志刷盘

我们上面说到的过程就是脏日志刷盘,上文提到了几个概念,这里做一些补充:

  • 日志缓冲(redo log buffer):存在易失性内存中的缓存日志
  • 日志文件(redo logfile):保存在磁盘上的redo log日志文件
  • fsync函数:用于同步内存中所有已修改的文件数据到储存设备。

为了确保每次记录都能够写入到磁盘中的日志中,每次将redo log buffer中的日志写入redo log file的过程中都会调用一次操作系统的fsync操作。在写入的过程中,还需要经过操作系统内核空间的os buffer。也就是我们上图的操作了。

脏数据刷盘

脏数据:指内存中未刷到磁盘的数据。

redo log的记录形式如下:

.jpg)

redo log 实际上记录数据页的变更,而这种变更记录是没必要全部保存,因此 redo log实现上采用了大小固定,循环写入的方式,当写到结尾时,会回到开头循环写日志。

redo log日志的大小是固定的,为了能够持续不断的对更新记录进行写入,在redo log日志中设置了两个标志位置,checkpoint和write_pos,分别表示记录擦除的位置和记录写入的位置。

  • 当write_pos标志到了日志结尾时,会从结尾跳至日志头部进行重新循环写入。所以redo log的逻辑结构并不是线性的,而是可看作一个圆周运动。write_pos与checkpoint中间的空间可用于写入新数据,写入和擦除都是往后推移,循环往复的。
  • 当write_pos追上checkpoint时,表示redo log日志已经写满。这时不能继续执行新的数据库更新语句,需要停下来先删除一些记录,执行checkpoint规则腾出可写空间。
    • checkpoint规则:checkpoint触发后,将buffer中脏数据页和脏日志页都刷到磁盘。

redo log中最重要的概念就是缓冲池buffer pool,这是在内存中分配的一个区域,包含了磁盘中部分数据页的映射,作为访问数据库的缓冲。

  • 当请求读取数据时,会先判断是否在缓冲池命中,如果未命中才会在磁盘上进行检索后放入缓冲池;
  • 当请求写入数据时,会先写入缓冲池,缓冲池中修改的数据会定期刷新到磁盘中。这一过程也被称之为刷脏 。

因此,当数据修改时,除了修改buffer pool中的数据,还会在redo log中记录这次操作;当事务提交时,会根据redo log的记录对数据进行刷盘。如果MySQL宕机,重启时可以读取redo log中的数据,对数据库进行恢复,从而保证了事务的持久性,使得数据库获得crash-safe能力。

  • MySQL中更新操作需要写进磁盘,磁盘也需要找到数据在更新,造成IO操作频繁而导致成本消耗高,所以有redo log,MySQL执行更新操作时先写日志,之后在空闲时写入磁盘,这样就能大大减少IO操作,提升数据刷新效率。
  • redo log记录了事务对数据页做的修改。
  • 脏数据刷盘:循环写入,有checkpoint和write pos标志,分别代表记录擦除的位置和记录写入的位置。write pos追上check point的时候代表日志写满了,这时候就不能继续执行新的数据库更新语句了
  • redo log缓冲池概念,在读取和写入时的不同
  • 保证事务的持久性,使得数据库获得crash-safe能力

二进制日志(binlog)

二进制日志binlog是服务层的日志,还被称为归档日志。binlog主要记录数据库的变化情况,内容包括数据库所有的更新操作。所有涉及数据变动的操作,都要记录进二进制日志中,以二进制的形式保存在磁盘中。因此有了binlog可以很方便的对数据进行复制和备份,因而也常用作主从库的同步

binlog 是 mysql的逻辑日志,并且由 Server 层进行记录,使用任何存储引擎的 mysql 数据库都会记录 binlog 日志。

  • 逻辑日志:可以简单理解为记录的就是sql语句 。
  • 物理日志:mysql 数据最终是保存在数据页中的,物理日志记录的就是数据页变更 。

binlog 是通过追加的方式进行写入的,可以通过max_binlog_size 参数设置每个 binlog文件的大小,当文件大小达到给定值之后,会生成新的文件来保存日志。

在实际应用中, binlog 的主要使用场景有两个,分别是 主从复制数据恢复

  • 主从复制 :在 Master 端开启 binlog ,然后将 binlog发送到各个 Slave 端, Slave 端重放 binlog 从而达到主从数据一致。
  • 数据恢复 :通过使用 mysqlbinlog 工具来恢复数据。

binlog 日志有三种格式,分别为 STATMENT 、 ROW 和 MIXED。

在 MySQL 5.7.7 之前,默认的格式是 STATEMENT , MySQL 5.7.7 之后,默认值是 ROW。日志格式通过 binlog-format 指定。

  • STATMENT:基于SQL 语句的复制( statement-based replication, SBR ),每一条会修改数据的sql语句会记录到binlog 中 。

    • 优点:不需要记录每一行的变化,减少了 binlog 日志量,节约了 IO , 从而提高了性能;
    • 缺点:在某些情况下会导致主从数据不一致,比如执行sysdate() 、 slepp() 等 。
  • ROW:基于行的复制(row-based replication, RBR ),不记录每条sql语句的上下文信息,仅需记录哪条数据被修改了 。

    • 优点:不会出现某些特定情况下的存储过程、或function、或trigger的调用和触发无法被正确复制的问题 ;
    • 缺点:会产生大量的日志,尤其是alter table 的时候会让日志暴涨
  • MIXED:基于STATMENT 和 ROW 两种模式的混合复制(mixed-based replication, MBR ),一般的复制使用STATEMENT 模式保存 binlog ,对于 STATEMENT 模式无法复制的操作使用 ROW 模式保存 binlog

在MySQL执行更新语句时,都会涉及到redo log日志和binlog日志的读写。一条更新语句的执行过程如下:

.jpg)

从上图可以看出,MySQL在执行更新语句的时候,在服务层进行语句的解析和执行,在引擎层进行数据的提取和存储;同时在服务层对binlog进行写入,在InnoDB内进行redo log的写入。不仅如此,在对redo log写入时有两个阶段的提交,一是binlog写入之前prepare状态的写入,二是binlog写入之后commit状态的写入。两阶段提交可以保证binlog和redo log中保存的信息是一致的。

这里binlog所存储的内容看起来似乎与redo log很相似,但是其实不然。redo log是一种物理日志,记录的是实际上对某个数据进行了怎么样的修改;而binlog是逻辑日志,记录的是SQL语句的原始逻辑,比如”给ID=2这一行的a字段加1 "。binlog日志中的内容是二进制的,根据日记格式参数的不同,可能基于SQL语句、基于数据本身或者二者的混合。一般常用记录的都是SQL语句。

同时,redo log是基于crash recovery,保证MySQL宕机后的数据恢复;而binlog是基于point-in-time recovery,保证服务器可以基于时间点对数据进行恢复,或者对数据进行备份。

总的来说,两者的区别可以归纳如下:

  • 日志类型

    • redo log是物理日志
    • binlog是逻辑日志
  • 对引擎的支持

    • redo log是innodb引擎特有的
    • binlog是通过MySQL的server层实现的,所有的引擎都能使用
  • 文件空间

    • redo log日志文件的空间是固定的
    • binlog日志文件的空间不固定,写完会切换下一个文件
  • 写入方式

    • redo log是循环写入和擦除
    • binlog日志是追加写入,不会覆盖已写文件

由 binlog 和 redo log 的区别可知:binlog 日志只用于归档,只依靠 binlog 是没有 crash-safe 能力的。

但只有 redo log 也不行,因为 redo log 是 InnoDB特有的,且日志上的记录落盘后会被覆盖掉。因此需要 binlog和 redo log二者同时记录,才能保证当数据库发生宕机重启时,数据不会丢失。

  • binlog是服务层的日志,主要用来记录数据库的变化情况,属于逻辑日志
  • 使用场景一般是主从复制和数据恢复
  • 执行过程是mysql执行更新语句的时候,在服务层进行语句的解析和执行,在引擎层进行数据的提取和存储;同时在服务层对binlog进行写入,在InnoDB内进行redo log的写入。
  • 还要记得binlog和redo log的区别

回滚日志(undo log)

回滚日志同样也是InnoDB引擎提供的日志,顾名思义,回滚日志的作用就是对数据进行回滚。当事务对数据库进行修改,InnoDB引擎不仅会记录redo log,还会生成对应的undo log日志;如果事务执行失败或调用了rollback,导致事务需要回滚,就可以利用undo log中的信息将数据回滚到修改之前的样子。

但是undo log不redo log不一样,它属于逻辑日志。它对SQL语句执行相关的信息进行记录。当发生回滚时,InnoDB引擎会根据undo log日志中的记录做与之前相反的工作。比如对于每个数据插入操作(insert),回滚时会执行数据删除操作(delete);对于每个数据删除操作(delete),回滚时会执行数据插入操作(insert);对于每个数据更新操作(update),回滚时会执行一个相反的数据更新操作(update),把数据改回去。undo log由两个作用,一是提供回滚,二是实现MVCC。