问题引出

有几个问题:

  • 日志通常情况下只是为了记录程序的运行状况,为什么在mysql中那么重要
  • mysql中日志的分类 并主要有什么作用
  • mysql记录的时机

    1、日志分类

    1.1、错误日志

    错误日志是 MySQL 中最重要的日志之一,它记录了当 mysqld 启动和停止时,以及服务器在运行过
    程中发生任何严重错误时的相关信息。 :::tips show variables like ‘%log_error%’%’ :::

1.2、二进制日志

1.2.1、介绍

二进制日志(BINLOG)记录了所有的 DDL(数据定义语言)语句和 DML(数据操纵语言)语句,但
不包括数据查询(SELECT、SHOW)语句
作用:①. 灾难时的数据恢复;②. MySQL的主从复制。在MySQL8版本中,默认二进制日志是开启着
的,涉及到的参数如下: :::tips show variables like ‘%log_bin%’; ::: image.png

log_bin_basename:当前数据库服务器的binlog日志的基础名称(前缀),具体的binlog文 件名需要再该basename的基础上加上编号(编号从000001开始)。 log_bin_index:binlog的索引文件,里面记录了当前服务器关联的binlog文件有哪些。

1.2.2、格式

STATEMENT 基于SQL语句的日志记录,记录的是SQL语句,对数据进行修改的SQL就会记录在日志文件中。
ROW 基于行的日志记录,记录的是每一行的数据变更(默认)
MIXED 混合了STATEMENT和ROW两种形式,默认采用STATEMENT,在某些时候自动切换成ROW进行记录。

:::tips show variables like ‘%binlog_format%’; ::: image.png
如果我们需要配置二进制日志的格式,只需要在 /etc/my.cnf 中配置 binlog_format 参数即
可。

1.2.3、查看

由于日志是以二进制方式存储,不能直接读取,需要通过二级制日志查询工具 mysqlbinlog 来查看

  1. mysqlbinlog [ 参数选项 ] logfilename
  2. 参数选项:
  3. -d 指定数据库名称,只列出指定的数据库相关操作。
  4. -o 忽略掉日志中的前n行命令。
  5. -v 将行事件(数据变更)重构为SQL语句
  6. -vv 将行事件(数据变更)重构为SQL语句,并输出注释信息

‘binlog.*’
purge master logs before
‘yyyy-mm-dd hh24:mi:ss

1.2.4、删除

由于日志是只要有操作就会进行记录,因此,如果长时间不清楚,会占用大量磁盘空间,可以通过以下几种方式进行清理:

reset 删除全部binlog日志,删除之后,将从binlog.000001重新开始
purge master logs to ‘binlog.*’ 删除 * 编号之前的所有日志
purge master logs before ‘yyyy-mm-dd hh24:mi:ss 删除时间之前产生的所有日志

也可以在mysql的配置文件中配置二进制日志的过期时间,设置了之后,二进制就会过期总动删除 :::tips show variables like ‘%binlog_expire_logs_seconds%’; :::

1.3、查询日志

查询日志中记录了客户端的所有操作语句,而二进制日志不包含查询数据的SQL语句。默认情况下,
查询日志是未开启的。
如果需要开启查询日志,可以修改MySQL的配置文件 /etc/my.cnf 文件,添加如下内容 :::tips general_log=1
general_log_file=mysql_query.log ::: 开启了查询日志之后,在MySQL的数据存放目录,也就是 /var/lib/mysql/ 目录下就会出现
mysql_query.log 文件。之后所有的客户端的增删改查操作都会记录在该日志文件之中,长时间运
行后,该日志文件将会非常大。

1.4、慢查询日志

慢查询日志记录了所有执行时间超过参数 long_query_time 设置值并且扫描记录数不小于
min_examined_row_limit 的所有的SQL语句的日志,默认未开启。long_query_time 默认为
10 秒,最小为 0, 精度可以到微秒。
如果需要开启慢查询日志,需要在MySQL的配置文件 /etc/my.cnf 中配置如下参数: :::tips slow_query_log=1
long_query_time=2 ::: 默认情况下,不会记录管理语句,也不会记录不使用索引进行查找的查询。可以使用
log_slow_admin_statements和 更改此行为 log_queries_not_using_indexes,如下所
述 :::tips log_slow_admin_statements = 1
log_query_not_using_indexes = 1 :::

1.5、总结

MySQL日志相关知识 - 图3

2、三大日志详解

除了上述的几种日志,在MySQL中还有 二进制日志 binlog(归档日志)和事务日志 redo log(重做日志)和 undo log(回滚日志)。
MySQL日志相关知识 - 图4

2.1、redo log

这个日志是MySQL所独有的日志类型,让其有了崩溃恢复能力。
比如如果MySQL 在运行过程中挂了或宕机,重启时,InnoDB 存储引擎会使用redo log 恢复数据,以此保证数据的持久性和完整性。
MySQL 中数据是以页(page)为单位,你查询一条记录,会从硬盘把一页的数据加载出来,加载出来的数据叫数据页,会放入到 Buffer Pool 中。
后续的查询都是先从 Buffer Pool 中找,没有命中再去硬盘加载,减少硬盘 IO 开销,提升性能。
更新表数据的时候,也是如此,发现 Buffer Pool 里存在要更新的数据,就直接在 Buffer Pool 里更新。
然后会把“在某个数据页上做了什么修改”记录到重做日志缓存redo log buffer)里,接着刷盘到 redo log 文件里。
image.png

每条 redo 记录由“表空间号+数据页号+偏移量+修改数据长度+具体修改的数据”组成

2.1.1、刷盘时机

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 刷盘。
image.png

fsync系统调用:需要你在入参的位置上传递给他一个fd,然后系统调用就会对这个fd指向的文件起作用。fsync会确保一直到写磁盘操作结束才会返回,所以当你的程序使用这个函数并且它成功返回时,就说明数据肯定已经安全的落盘了。所以fsync适合数据库这种程序。

image.png
也就是说,一个没有提交事务的 redo log 记录,也可能会刷盘。
为什么呢?
因为在事务执行过程 redo log 记录是会写入redo log buffer 中,这些 redo log 记录会被后台线程刷盘。
image.png
除了后台线程每秒1次的轮询操作,还有一种情况,当 redo log buffer 占用的空间即将达到 innodb_log_buffer_size 一半的时候,后台线程会主动刷盘。

下面是不同刷盘策略的流程图。

# innodb_flush_log_at_trx_commit=0

image.png
为0时,如果MySQL挂了或宕机可能会有1秒数据的丢失。

# innodb_flush_log_at_trx_commit=1

image.png
为1时, 只要事务提交成功,redo log记录就一定在硬盘里,不会有任何数据丢失。
如果事务执行期间MySQL挂了或宕机,这部分日志丢了,但是事务并没有提交,所以日志丢了也不会有损失。

# innodb_flush_log_at_trx_commit=2

image.png
为2时, 只要事务提交成功,redo log buffer中的内容只写入文件系统缓存(page cache)。
如果仅仅只是MySQL挂了不会有任何数据丢失,但是宕机可能会有1秒数据的丢失。

2.1.2、日志文件组

硬盘上存储的 redo log 日志文件不只一个,而是以一个日志文件组的形式出现的,每个的redo日志文件大小都是一样的。
比如可以配置为一组4个文件,每个文件的大小是 1GB,整个 redo log 日志文件组可以记录4G的内容。
它采用的是环形数组形式,从头开始写,写到末尾又回到头循环写,如下图所示。
image.png
在个日志文件组中还有两个重要的属性,分别是 write pos、checkpoint

  • write pos 是当前记录的位置,一边写一边后移
  • checkpoint 是当前要擦除的位置,也是往后推移

每次刷盘 redo log 记录到日志文件组中,write pos 位置就会后移更新。
每次 MySQL 加载日志文件组恢复数据时,会清空加载过的 redo log 记录,并把 checkpoint 后移更新。
write pos 和 checkpoint 之间的还空着的部分可以用来写入新的 redo log 记录。
image.png
如果 write pos 追上 checkpoint ,表示日志文件组满了,这时候不能再写入新的 redo log 记录,MySQL 得停下来,清空一些记录,把 checkpoint 推进一下。
image.png

2.1.3、redo log 小结

相信大家都知道 redo log 的作用和它的刷盘时机、存储形式。
现在我们来思考一个问题: 只要每次把修改后的数据页直接刷盘不就好了,还有 redo log 什么事?
它们不都是刷盘么?差别在哪里?

  1. 1 Byte = 8bit
  2. 1 KB = 1024 Byte
  3. 1 MB = 1024 KB
  4. 1 GB = 1024 MB
  5. 1 TB = 1024 GB

实际上,数据页大小是16KB,刷盘比较耗时,可能就修改了数据页里的几 Byte 数据,有必要把完整的数据页刷盘吗?
而且数据页刷盘是随机写,因为一个数据页对应的位置可能在硬盘文件的随机位置,所以性能是很差。
如果是写 redo log,一行记录可能就占几十 Byte,只包含表空间号、数据页号、磁盘文件偏移 量、更新值,再加上是顺序写,所以刷盘速度很快。
所以用 redo log 形式记录修改内容,性能会远远超过刷数据页的方式,这也让数据库的并发能力更强。
其实内存的数据页在一定时机也会刷盘,我们把这称为页合并,讲 Buffer Pool的时候会对这块细说

2.2、binlog

redo log 它是物理日志,记录内容是“在某个数据页上做了什么修改”,属于 InnoDB 存储引擎。
而 binlog 是逻辑日志,记录内容是语句的原始逻辑,类似于“给 ID=2 这一行的 c 字段加 1”,属于MySQL Server 层。
不管用什么存储引擎,只要发生了表数据更新,都会产生 binlog 日志。
那 binlog 到底是用来干嘛的?
可以说MySQL数据库的数据备份、主备、主主、主从都离不开binlog,需要依靠binlog来同步数据,保证数据一致性。
image.png
binlog会记录所有涉及更新数据的逻辑操作,并且是顺序写。

# 记录格式

binlog 日志有三种格式,可以通过binlog_format参数指定。

  • statement
  • row
  • mixed

指定statement,记录的内容是SQL语句原文,比如执行一条update T set update_time=now() where id=1,记录的内容如下。
image.png
同步数据时,会执行记录的SQL语句,但是有个问题,update_time=now()这里会获取当前系统时间,直接执行会导致与原库的数据不一致
为了解决这种问题,我们需要指定为row,记录的内容不再是简单的SQL语句了,还包含操作的具体数据,记录内容如下。
image.png
row格式记录的内容看不到详细信息,要通过mysqlbinlog工具解析出来。
update_time=now()变成了具体的时间update_time=1627112756247,条件后面的@1、@2、@3 都是该行数据第 1 个~3 个字段的原始值(假设这张表只有 3 个字段)。
这样就能保证同步数据的一致性,通常情况下都是指定为row,这样可以为数据库的恢复与同步带来更好的可靠性。
但是这种格式,需要更大的容量来记录,比较占用空间,恢复与同步时会更消耗IO资源,影响执行速度。
所以就有了一种折中的方案,指定为mixed,记录的内容是前两者的混合。
MySQL会判断这条SQL语句是否可能引起数据不一致,如果是,就用row格式,否则就用statement格式。

# 写入机制

binlog的写入时机也非常简单,事务执行过程中,先把日志写到binlog cache,事务提交的时候,再把binlog cache写到binlog文件中。
因为一个事务的binlog不能被拆开,无论这个事务多大,也要确保一次性写入,所以系统会给每个线程分配一个块内存作为binlog cache。
我们可以通过binlog_cache_size参数控制单个线程 binlog cache 大小,如果存储内容超过了这个参数,就要暂存到磁盘(Swap)。
binlog日志刷盘流程如下
image.png

  • 上图的 write,是指把日志写入到文件系统的 page cache,并没有把数据持久化到磁盘,所以速度比较快
  • 上图的 fsync,才是将数据持久化到磁盘的操作

write和fsync的时机,可以由参数sync_binlog控制,默认是0。
为0的时候,表示每次提交事务都只write,由系统自行判断什么时候执行fsync。
image.png
虽然性能得到提升,但是机器宕机,page cache里面的 binlog 会丢失。
为了安全起见,可以设置为1,表示每次提交事务都会执行fsync,就如同 redo log 日志刷盘流程 一样。
最后还有一种折中方式,可以设置为N(N>1),表示每次提交事务都write,但累积N个事务后才fsync。
image.png
在出现IO瓶颈的场景里,将sync_binlog设置成一个比较大的值,可以提升性能。
同样的,如果机器宕机,会丢失最近N个事务的binlog日志

2.3、两阶段提交

redo log(重做日志)让InnoDB存储引擎拥有了崩溃恢复能力。
binlog(归档日志)保证了MySQL集群架构的数据一致性。
虽然它们都属于持久化的保证,但是侧重点不同。
在执行更新语句过程,会记录redo log与binlog两块日志,以基本的事务为单位,redo log在事务执行过程中可以不断写入,而binlog只有在提交事务时才写入,所以redo log与binlog的写入时机不一样。
image.png
回到正题,redo log与binlog两份日志之间的逻辑不一致,会出现什么问题?
我们以update语句为例,假设id=2的记录,字段c值是0,把字段c值更新成1,SQL语句为update T set c=1 where id=2。
假设执行过程中写完redo log日志后,binlog日志写期间发生了异常,会出现什么情况呢?
image.png
由于binlog没写完就异常,这时候binlog里面没有对应的修改记录。因此,之后用binlog日志恢复数据时,就会少这一次更新,恢复出来的这一行c值是0,而原库因为redo log日志恢复,这一行c值是1,最终数据不一致。
image.png
为了解决两份日志之间的逻辑一致问题,InnoDB存储引擎使用两阶段提交方案。
原理很简单,将redo log的写入拆成了两个步骤prepare和commit,这就是两阶段提交
image.png
使用两阶段提交后,写入binlog时发生异常也不会有影响,因为MySQL根据redo log日志恢复数据时,发现redo log还处于prepare阶段,并且没有对应binlog日志,就会回滚该事务。
image.png
再看一个场景,redo log设置commit阶段发生异常,那会不会回滚事务呢?
image.png
并不会回滚事务,它会执行上图框住的逻辑,虽然redo log是处于prepare阶段,但是能通过事务id找到对应的binlog日志,所以MySQL认为是完整的,就会提交事务恢复数据。

2.4、undo log

这部分内容为 JavaGuide 的补充:
我们知道如果想要保证事务的原子性,就需要在异常发生时,对已经执行的操作进行回滚,在 MySQL 中,恢复机制是通过 回滚日志(undo log) 实现的,所有事务进行的修改都会先记录到这个回滚日志中,然后再执行相关的操作。如果执行过程中遇到异常的话,我们直接利用 回滚日志 中的信息将数据回滚到修改之前的样子即可!并且,回滚日志会先于数据持久化到磁盘上。这样就保证了即使遇到数据库突然宕机等情况,当用户再次启动数据库的时候,数据库还能够通过查询回滚日志来回滚将之前未完成的事务。
另外,MVCC 的实现依赖于:隐藏字段、Read View、undo log。在内部实现中,InnoDB 通过数据行的 DB_TRX_ID 和 Read View 来判断数据的可见性,如不可见,则通过数据行的 DB_ROLL_PTR 找到 undo log 中的历史版本。每个事务读到的数据版本可能是不一样的,在同一个事务中,用户只能看到该事务创建 Read View 之前已经提交的修改和该事务本身做的修改

2.5、总结

这部分内容为 JavaGuide 的补充:
MySQL InnoDB 引擎使用 redo log(重做日志) 保证事务的持久性,使用 undo log(回滚日志) 来保证事务的原子性
MySQL数据库的数据备份、主备、主主、主从都离不开binlog,需要依靠binlog来同步数据,保证数据一致性

文章推荐

「原创」从RedoLog和BinLog聊到一条Update语句的执行
「原创」竟然redolog写入机制都不懂…怎么破?
一文读懂MySQL的BinLog写入机制|原创