在前面的分析 MySQL 的存储引擎相关的文章中,已经明确知道了在数据记录在以 16KB 的页进存储的,当操作 SQL 时数据库会针对数据所在的页面进行增删改查操作,读页面、写页面、创建新页面。在数据库执行事务的过程中会将所需要的数据页读取到内存中的 Buffer Pool 中进行操作,这样就使得内存中对事务进行隔离运算,当完成了数据更新操作就会进行刷盘持久化,这是一个完整的事务生命周期。

当在内存中 Buffer Pool 的修改页面时,突发发生断电情况,那么内存中的数据就会丢失,如果数据未提交持久化就发生丢失情况,如何保证数据不丢失?最简单的办法记录日志,将在内存中对数据产生修改的页面信息记录到一个日志中,通过日志的方式好处是不需要对整个页面进行刷盘操作产生的随机 IO 性能问题,日志文件系统都是以 Append Only 方式进行的。


REDO LOG

为了防止在执行事务过程中程序突然崩溃导致内存数据丢失,MySQL 的设计者在引入 Redo 日志的设计,即使程序崩溃也能通过 Redo 日志里面信息恢复数据。Redo Log 相比传统数据页占用的内存空间非常小,只会存储表空间的 ID 和 页面号、和具体修改的数据偏移量,和被修改数据的新值,Redo Log 和之前博客中介绍的 Bitcask 存储引擎很相似。一条通用的 Redo Log 日志结构如下,Type 日志类型、Space ID 表空间、Page Number 页号、Data 真实的数据,至于 Len 和 offset 针对的是一些特殊类型的日志记录的附加信息:

在 Redo Log 中的 Type 字段的类型有 53 种,日志数据的类型是根据页面中写入的数据类型进行划分的,整理一个常见的类型表格:

类 型作用说明
MLOG_1BYTE对应十进制数字为 1 表示在偏移量处写 1 字节
MLOG_2BYTE对应十进制数字为 2 表示在偏移量处写 2 字节
MLOG_4BYTE对应十进制数字为 4 表示在偏移量处写 4 字节
MLOG_8BYTE对应十进制数字为 8 表示在偏移量处写 8 字节
MLOG_WRITE_STRING对应十进制数字为 30 表示在偏移量处写入一个字节序列

设计这么多的 Type 类型是为了节省空间,能劲量不要使用 offset 和 len 字段时就劲量不要使用,提高空间利用率积少成多,其 Type 字段类型远远不止本文中列出来的,能节省一个字节是一个字节。在 Redo Log 日志里面不仅仅有物理日志,还有一部分的逻辑日志,逻辑日志针对事务操作的类型日志,Type 类型是数据操作的动作,具体其他类型可以查看其他资料

在 MySQL 默认的存储引擎 Redo Log 大小可以通过命令进行查看,默认是 16MB 被存储引擎提前申请好存储在 /var/lib/mysql 目录中:

SHOW VARIABLES LIKE 'innodb_log_buffer_size';

当一个事务执行时 Redo Log 的工作原理图为下图,先是解析 SQL 语句,生成执行器然后去执行器里面执行加载需要的数据页,然后开始记录事务操作逻辑日志信息:

正如上图 Redo Log 作为文件存储在磁盘上,那么就需要将内存中的日志操作记录刷入到磁盘文件里面,因为数据库是用户态下的程序所以他文件写入依赖于操作系统的文件缓存的刷入频率,Innodb 提供多种刷盘策略,默认 MySQL 的刷盘策略可以通过下面命令查看:

SHOW VARIABLES LINK 'innodb_flush_log_at_trx_commit'

分有 3 种策略,默认值为 1 下图:

目前已 3 种值类型操作,分别为下面列表中:

值类型作用说明
0每次事务提交时不进行强行执行刷入磁盘,而是根据后台的线程进行的
1每次事务提交时都会执行一次刷入磁盘的操作
2每次事务提交时只把内存缓冲区的数据刷入到系统的 Page Cache 中依赖于系统

在日志中还有一种特别的类型为 Mini-Transaction 类型,它是指一次事务对 Innodb 底层数据页的一次原子访问的过程。也就是说在一张有索引表中插入一条记录,那么聚族索引 B+ 树的数据页也会插入这条记录的索引信息,这个过程就称为对底层页的一次原子访问,如果这个过程中运行到一半就宕机了,如果 Redo Log 日志只记录一部分日志不是完整的,下次恢复数据就不能恢复完整的数据记录,所以这里的存储引擎设计者设计一组 Redo Log 要么把全部的日志都恢复掉,要么就一条也不恢复,结构如下图:

在 Redo Log Buffer 中的多个 Mini-Transaction 可以组成一个 Log Block 块,多而个 Log Block 又可以组成 Redo Log Buffer,而负责存储 Mini-Transaction 数据的是 Log Body,Log Body 可以存储多条 Mini-Transaction,每块 Log Block 中都有 buf_free 指向这里该位置之后就是空闲的区域,每当执行一个 MTR 过程中会产生多个 Redo 日志都不可以分割的组,属于同一组的日志才能归属为同一组的 MTR 所有,运行过程中 MTR 只能在内存中,直到 MTR 执行结束复制到对应的 Log Block 中持久化。

在 Log Block 分为 3 个部分,分别为 Header 、 Body 、和 Trailer,其中 Header 最为数据源信息部分存储每个 Log Block 一些基本信息,而 Trailer 为防止数据丢失或者损坏查错使用的校验码。当 Log Buffer 刷入到磁盘上会有多个日志文件组保存,而这些文件组的数量是固定的,并且文件组的总体占用大小限制为 innodb_log_file_size * innodb_log_files_in_group ,当要写入日志时如果发现大小不够就会从头开始覆盖文件,多个文件会形成一个环路,这个稍作了解即可。

Rode 磁盘日志文件格式对应着内存中的 Log Buffer 本质上也是一片连续的存储空间, 被划分成了多个 512 字节大小的 Block,当持久化时也就是将内存的缓冲区的 Block 刷写到磁盘上的日志文件中,整个磁盘上的日志文件可以看成是内存中的多个 512 字节的大小组成一个链表结构。

多个日志文件组成一个日志文件组,每个大小和数据组织格式方式也是相同的,前 2048 也就是前 4 个 Block 对应存储着每个日志文件管理信息,从 2048 以后为真实的 Block 数据信息,多个文件日志文件形成了一个环,下图:

日志文件最核心功能即为保证系统崩溃或者断电情况下,在数据库恢复正常时还能恢复数据,如果一个事务在执行完成之后内存中的 Buffer Pool 脏页面已经被刷入到磁盘中,则 Redo Log 中对应的 Block 数据块已经毫无存在的意义,那么其所占用空间可以被其他日志覆盖重用,多个日志文件组会形成一个环形以此达成存储空间复用。

图上的 2 个日志文件组的分割线在代码实现层面是 2 个变量,分为 checkpoint 和 write_pos,checkpoint 之前的数据页已经被刷写到磁盘中, write_pos 之后的数据页为当前需要刷写的,checkpoint 和 write_pos 之间的数据页新数据页可以插入的位置。


UNDO LOG

上面的 Redo Log 是属于帮助数据库的事务提供 持久化 的保证一种方案,而 Undo Log 要为数据提供一种数据在某个时间段的数据 一致性 方案,Undo Log 的数据信息属于逻辑级别的,会记录一条数据在被修改前的数据方便事务进行回滚操作,数据库有 Undo Log 的支持才能在执行过程中可以有退回之前数据版本的选项。当像已经存在的某条数据进行修改操作时 Undo Log 就会针对该条数据进行记录相反的操作,例如 执行 DELETE 一条数据的事务,而 Undo Log 会生成一条对应的 INSERT 事务操作,方便后面回滚使用,核心规则有 4 条:

  1. 在插入一条记录时,至少要吧这条记录的主键值记录下来,如果发生回滚操作可以删除对应值的主键记录。
  2. 在删除一条数据记录时会记录当前记录数据,备份当前记录的数据到日志中方便后面回滚恢复数据,重新插入到表中。
  3. 在修改一条记录时,至少要把被更新记录的值记录下来,方便后面回滚到旧值的版本。
  4. 在执行 SELECT 事务如果是只读事务则不需要进行任何操作,但是 MVCC 模式下可以通过旧值提高读取。

在 MySQL 中实现可重复读级别隔离的事务时,大部分数据库采用的 MVCC 多版本并发控制,而实现 MVCC 最主要的基础就是 Undo Log 来实现的,当一个事务读取一行记录时若改记录已经被其他事务占用,则当前事务可以读存储在 Undo Log 中的之前的行记录版本,从而事务非锁定并行多事务读取行记录。


BIN LOG

BinLog 相比上面的 Redo Log 和 Undo Log 两种日志完全不同,BinLog 是 MySQL Sever 层维护的一种二进制日志,可以帮助多台主从架构的 MySQL Server 能正常复制表数据记录,其主要是用来记录对 MySQL 数据更新或潜在发生更新的 SQL 语句,并以事务的形式保存在磁盘中。它记录了所有的 DDL 和 DML 语句,除了数据查询语句 SELETE 、SHOW 等语句,以事件形式记录,还包含语句所执行的消耗的时间,MySQL 的二进制日志是事务安全型的,主从架构下可以使用 Master 把它的二进制日志传递给 Slave 并回放来达到 Master 节点和 Slave 节点数据一致的目的。

默认情况下数据库服务器会自动开启 BinLog 功能,相关的设置可以在配置文件中编写,可以配置参数有很多,一般默认情况下的配置:

[mysqld]
# 二进制日志的存储目录
log-bin=/home/mysql/binlog/

# 高版本MySQL需要server-id这个参数,提供一个集群中不重复的id值即可
server-id=1

# mysql-bin.*日志文件最大字节(单位:字节)
# 设置最大100MB
max_binlog_size=104857600

# 设置了只保留7天BINLOG(单位:天)
expire_logs_days = 7

# binlog日志只记录指定库的更新
# binlog-do-db=db_name

# binlog日志不记录指定库的更新
# binlog-ignore-db=db_name

# 写缓冲多少次,刷一次磁盘,默认0
sync_binlog=0

查看 MySQL 有没有开启使用下面命令即可查询相关的参数信息:

SHOW VARIABLES LINK `%log_bin%`;

如果开启 BinLog 后相关的日志文件会在数据目录中,可以使用下面命令查看数据目录文件列表:

SHOW BINARY LOGS; 

如果开启的主从架构相关的可以使用下面命令查看日志的状态信息:

SHOW MASTER STATUS;

默认 MySQL 生成的 BinLog 是采用的 ROW 模式,这里的复制模式和之前我技术文章中的分布式数据复制中讲解的概念很类似,在 MySQL 中提供了 3 中模式:

  1. STATEMENT:基于SQL语句的复制(statement-based replication, SBR)
  2. ROW:基于行的复制(row-based replication, RBR)
  3. MIXED:混合模式复制(mixed-based replication, MBR)

在 MySQL 5.7.7 版本之后的默认都是采用 ROW 模式进行的,如果单纯基于 SQL 语句的复制会出现一些问题,不能使用一些特殊的 SQL 内置函数,这个问题之前的文章中我也讲过,日志格式通过 binlog-format 进行设置。


其他资料

便宜 VPS vultr
最后修改:2023 年 07 月 05 日
如果觉得我的文章对你有用,请随意赞赏 🌹 谢谢 !