在MySQL InnoDB 这个语境下, crash safe、数据不丢失 都指的是事务的持久性特性,即事务一旦提交,应当保证所有被成功提交的数据修改都能够正确地被持久化,不丢失数据, 即使宕机也能够恢复数据

在InnoDB 中,持久性 基于binlog 和redo log 实现, 且binlog 与redo log 的写入通过2PC 协调.

0 XA 事务:binlog 和redo log 的两阶段提交

在MySQL中,InnoDB存储引擎 的 redo log 和MySQL服务器层binlog 之间的一致性是通过内部的XA机制(即分布式事务)来实现的,任何一个数据出现问题都会进行会滚。

XA事务是一种分布式事务。通过两阶段提交协议和XA接口标准,事务管理器和资源管理器能够可靠地协同工作,实现跨系统的事务处理,确保多个独立资源的一致性。

在binlog 和redo log 的两阶段提交, binlog 充当协调者的角色。

关于XA 事务具体可在这篇文章中查看

binlog 和 redo log 各自写入的过程还有很多细节,接下来进行讲解

1 binlog

binlog是 MySQL 服务器层使用的日志文件,记录了所有修改数据库内容的SQL语句(如 INSERT, UPDATE, DELETE),也被称为逻辑日志。

binlog 主要用于主备复制同步、崩溃恢复等功能。

1.1 binlog 的三种日志格式

格式 定义 优点 缺点
Statement-Based Logging (SBL) 记录执行的 SQL 语句本身,而不是每行数据的变更。 1. 空间效率高:通常占用更少的空间,因为记录的是 SQL 语句。
2. 易于审计:直接记录 SQL 语句,易于阅读和理解。
1. 非确定性行为:可能在主从复制中导致数据不一致,特别是涉及到非确定性函数(如 NOW()、RAND())的 SQL 语句。
2. 复制错误:某些特定情况下可能引起从服务器的复制错误。
Row-Based Logging (RBL) 记录数据变更前后的每行数据的具体变化,而不是执行的 SQL 语句。 1. 数据一致性:在复制过程中提供高度的数据一致性。
2. 安全性更高:不记录 SQL 语句,降低了 SQL 注入的风险。
1. 空间占用大:因为记录了每一行的变化,可能导致 binlog 文件迅速增大。
2. 可读性差:不记录 SQL 语句,对于人类审计不友好。
Mixed-Based Logging (MBL) 结合了 SBL 和 RBL 的特点,根据操作的类型自动选择使用基于语句的格式或基于行的格式记录。 1. 灵活性高:根据 SQL 语句的特性选择最合适的日志格式。
2. 平衡性能和一致性:在确保数据一致性的同时考虑日志大小和性能。
1. 配置复杂:需要适当配置以确保效率和准确性。
2. 预测性差:自动切换日志格式可能使得日志的结果难以预测。

1.2 binlog写入过程

binlog 的写入逻辑比较简单:事务执行过程中,先把日志写到 binlog cache,事务提交的时候,再把 binlog cache 写到 binlog 文件中。

1.2.1 binlog cache

对于每个客户端会话,MySQL 服务器为其分配一个 binlog cache。这个缓存是用来临时存储一个事务中产生的所有 binlog 事件。 但是binlog cache 刷新到磁盘时 多个线程是共写同一份 binlog 文件。

当一个新事务开始时,根据binlog 日志格式记录 每个修改SQL 语句到binlog cache 中

1.2.2 page cache 与 磁盘刷新持久化

当事务到达提交阶段时,首先将 binlog cache 中的内容 写入到binlog 文件中,然后提交事务到 InnoDB,即 commit redo log 。

注意,这里的写入并不是直接写到到磁盘,而是先写入到文件系统的page cache, 然后通过sync_binlog 参数来决定 何时把数据写入到 磁盘。

磁盘刷新频率通过 sync_binlog 配置参数,

  1. sync_binlog=0 的时候,表示每次提交事务都不主动刷新磁盘,由文件系统自己控制刷盘频率
  2. sync_binlog=1 的时候,表示每次提交事务都会将 binlog cache 中的内容刷新到磁盘
  3. sync_binlog=N(N>1) 的时候,表示累积 N 个提交事务后才将多个binlog cache中的内容刷新到磁盘。

可以看到如果sync_binlog不设置为1 ,有有助于提高刷盘效率, 但是有丢失binlog 的风险。

1.2.3 binlog cache 不够用怎么办

如果binlog cache 写满了怎么办?需要把数据暂存到磁盘

每个事务的 binlog 事件首先被写入到 binlog cache 中,这个缓存的大小由 binlog_cache_size 系统变量控制。

如果一个事务非常大,涉及大量的数据修改,导致binlog cache不足以存储当前事务的所有事件时,MySQL采用的处理机制是将缓存中的数据写入到磁盘上的一个临时文件中。这一过程可以分为以下几个步骤:

  1. 检测缓存溢出:当试图向binlog cache中写入数据,而缓存空间不足以容纳更多数据时,将触发溢出处理机制。
  2. 数据写入临时文件:MySQL将当前binlog cache中的数据写入到一个临时文件中。这个临时文件通常位于MySQL的数据目录下,具有唯一标识,确保数据的隔离和安全。
  3. 清空binlog cache:将数据写入临时文件后,binlog cache会被清空,为接下来的日志数据腾出空间。
  4. 继续事务日志的记录:事务继续执行,新的日志事件会再次被记录到现在已经被清空的binlog cache中。
  5. 事务提交:事务如果最终被提交,MySQL会将临时文件中的日志数据以及现在binlog cache中的数据一并写入到全局的binlog文件中。如果事务回滚,则临时文件和binlog cache中的数据都将被丢弃。

1.3 xid

XID(Transaction Identifier) 可以理解成时MySQL server 层的事务唯一标识。

  • MySQL服务器内部维护一个全局事务ID计数器,每个新事务都会分配一个唯一的ID。该计数器在内存中递增,保证每个事务ID在实例中是唯一的。
  • 当一个新事务开始时,MySQL服务器层会从全局计数器中获取一个新的事务ID,将其赋予该事务,并存储在该事务的上下文中。

2 redo log

redo log是 InnoDB 存储引擎特有的日志文件,用于记录对数据库做出的更改前的数据页状态,也被称作物理日志,确保在数据库系统发生崩溃后能够恢复这些更改。
记录内容:Redo log 记录的是数据页修改的物理操作,而非具体的 SQL 语句。

  • 循环使用:Redo log 是固定大小的,通常配置为一组文件,工作在循环写入的方式。
  • 崩溃恢复:系统重启后,InnoDB 通过回放 redo log 来恢复未完成的事务,确保数据的完整性和一致性。
  • 提高性能:Redo log 允许 InnoDB 在事务提交时不必将所有数据页写回磁盘,只需确保 redo log 已被写入磁盘。
  • 记录的是数据页的物理修改。 不论数据页是否在buffer pool 中, redo log 都要记录修改, 因为不记不能保证crash safe.
  • 保存自增值

2.1 为什么要记录redo log

2.1.1 buffer pool

MySQL 为了实现高性能,是不可能每次都从磁盘读数据或者把对数据的修改持久化到磁盘上的,所以 InnoDB 申请了一块连续的内存,用于存储从磁盘上读取的pages, 这个内存就是buffer pool。

buffer pool 有一块内存叫做,change buffer 用于暂存对数据的修改

那么在修改数据时,就会遇到两种情况

  1. 数据所在的page 在buffer pool 中, 就会直接更新page
  2. 数据所在的page 不在buffer pool 中, 如果不需要加载对应page, 就会先把对数据的修改先记在change buffer 中

不论是buffer pool, 还是 buffer pool 中的change buffer, 都是内存,一旦发生宕机,那就数据的修改的修改就会丢失,此时就违背了事务的持久性。

为了能把修改过的数据持久化又不影响性能,InnoDB 给出的方案是优先把修改操作记下来并持久化, 事务提交后,万一宕机丢失了buffer pool 中已修改但是未持久化的内容,就可以根据持久化的修改操作重新得到修改后数据。

这里记录下来的修改操作就是redo log, 而这种先记录修改操作,再记录修改后的技术叫做WAL。

2.1.2 WAL

WAL(Write-Ahead Logging)是一种在数据库系统中广泛采用的日志管理技术,用于保证数据库的事务持久性和恢复能力。

它的关键点就是先写日志,再写真正的数据。

redo log 直接应用了 WAL 技术,确保在任何数据被写入数据库页之前,相应的日志信息(如数据页的修改)先被写入到 redo log 中。

总的来说WAL 技术的优势有以下3项,

  1. 恢复能力:WAL 提供了强大的数据恢复能力。在发生系统故障后,可以利用日志文件中的记录来重做或撤销事务,恢复到最后一致的状态。
  2. 性能优化:通过将对磁盘数据的随机写转换为顺序写 , 同时利用 组提交 ,WAL 可以显著提高数据库的写性能。
  3. 事务原子性和持久性:WAL 通过确保所有日志记录在实际数据写入前被提交到磁盘,从而支持数据库事务的原子性和持久性。

2.2 redo log 记录的内容

之所以说redo log 是物理日志, 是因为其记录了对特定数据page 数据的修改。
该例子来自极客专栏《MySQL 实战45讲》

1
2
mysql> create table t(ID int primary key, c int);
mysql> insert into t(id,k) values(id1,k1),(id2,k2);

这条更新语句做了如下的操作(按照图中的数字顺序):

  1. Page 1 在内存中,直接更新内存;
  2. Page 2 没有在内存中,就在内存的 change buffer 区域,记录下“我要往 Page 2 插入一行”这个信息
  3. 将上述两个动作记入 redo log 中(图中 3 和 4)。

Redo log不是记录数据页“更新之后的状态”,而是记录这个页 “做了什么改动”。

2.3 redo log 写入过程

# 23 | MySQL是怎么保证数据不丢的? redo log 的写入机制-redo log buffer

redo log 的写入机制和 binlog 类型, 需要经历

  1. MySQL 系统内存cache , redo lo buffer
  2. 文件系统page cache
  3. 刷新持久化到磁盘

2.3.1 redo log buffer

add(id1,k1) to page1, new change buffer item add(id2,k2) to page2 都是先写入redo log buffer 中

相比较 每个线程都拥有自己一块独立的 binlog cache , 而 redo log buffer 是全局共用的。

2.3.2 redo log持久化到磁盘

事务提交,执行commit redo log 后,会触发redo log buffer 中内容写入到redo log 中。

为了控制 redo log 的写入策略,InnoDB 提供了 innodb_flush_log_at_trx_commit 参数,它有三种可能取值:

  1. 设置为 0 的时候,表示每次事务提交时都只是把 redo log 留在 redo log buffer 中 ;
  2. 设置为 1 的时候,表示每次事务提交时都将 redo log 直接持久化到磁盘;
  3. 设置为 2 的时候,表示每次事务提交时都只是把 redo log 写到 page cache。

所以想要确保MySQL异常重启之后redo log 数据不丢失,innodb_flush_log_at_trx_commit 这个参数 建议设置成1.

前面在binlog部分说到, 在事务提交前,事务binlog 是不会被写入到真正的binlog 文件中的。 redo log 不一样,在事务提交前,redo log 有可能备持久化磁盘。有以下3种情况

  1. 后台线程,每隔 1 秒,就会把 redo log buffer 中的日志,调用 write 写到文件系统的 page cache,然后调用 fsync 持久化到磁盘。,
  2. redo log buffer 占用的空间即将达到 innodb_log_buffer_size 一半的时候,后台线程会主动写盘。注意,由于这个事务并没有提交,所以这个写盘动作只是 write,而没有调用 fsync,也就是只留在了文件系统的 page cache。
  3. 并行的事务提交的时候,顺带将这个事务的 redo log buffer 持久化到磁盘。如果 innodb_flush_log_at_trx_commit 设置的是 1,那么按照这个参数的逻辑, 要把 redo log buffer 里的日志全部持久化到磁盘。这时候,就会带上未提交事务 在 redo log buffer 里的日志一起持久化到磁盘。

    2.3.3 2PC的细化过程

2.4 日志文件组

InnoDB 的 redo log 是以日志文件组的形式组织的。一个日志文件组通常包含两个或更多的日志文件,这些文件在物理上是连续的,并且循环使用。当一个日志文件写满后,InnoDB 会自动切换到下一个日志文件继续写入。当最后一个文件写满后,它会回到第一个文件并开始覆盖旧的日志记录,这就是所谓的“环形写入”。

2.5 LSN

LSN(Log Sequence Number),日志序列号,是一个不断增长的全局变量, 用来记录当前redo log 文件中 已经写入的日志量, 单位是字节。

图片中的write pos LSN 指当前已经产生的的日志量,随着更多的事务数据被写入,write pos LSN 会不断增加

checkpoint LSN 是redo log 中的一个位置,表示所有之前的日志记录都已经被应用(或说是“刷新”)到了磁盘的数据页上,因此,从这个位置以前的日志数据可以安全地被覆写, 不会出现数据丢失的情况。 redo log 会有多个检查点

write pos LSN 和 checkpoint LSN之间空着的部分,可以用来记录新的操作。

如果 write pos LSN 赶上了最一个checkpoint LSN 位置,这意味着 redo log 的空间不足,可能会导致数据库操作停顿,因为系统需要等待足够的日志空间来记录新的事务数据。

2.6 组提交

前面提过,redo log 提升性能,一个是把对磁盘的随机写转换成了顺序写,一个是组提交机制。

组提交机制(Group Commit)是一种通过合并多个事务的日志提交操作来提高I/O效率的策略。这一机制基于LSN(Log Sequence Number,日志序列号)来追踪和管理日志提交。

以下图为例解释

  1. 事务trx1开始
    • trx1进入事务队列并被选为组的领导者,日志记录的LSN开始增加。
  2. 事务trx2trx3加入
    • trx1进入队列之后,trx2trx3紧随其后进入提交队列。
  3. LSN更新到160
    • 随着trx2trx3的日志写入缓冲区,整个组的最后一个日志序列号LSN变为160。
  4. 领导者trx1执行写盘
    • trx1作为组的领导者,携带LSN=160去执行一次性日志写盘(fsync)操作。
  5. 写盘完成
    • trx1的fsync操作完成后,所有LSN <= 160的日志记录都被持久化到磁盘。
  6. 事务返回提交成功
    • trx1trx2trx3都标记为提交成功并从提交队列中移除。

3 事务执行过程中的binlog 和redolog 和undo log

下面将结合MySQL 的逻辑架构 和具体SQL , 来具体地看一下binlog 和redo log 的写入

SQL

1
2
mysql> create table T(ID int primary key, c int);
mysql> update T set c=c+1 where ID=2;

结果MySQL 的逻辑架构, 该update sql的执行过程如下

  1. 执行器先找InnoDB取 ID=2 这一行。ID 是主键,引擎直接用树搜索找到这一行。如果 ID=2 这一行所在的数据页本来就在buffer pool 中,就直接返回给执行器;否则,需要先从磁盘读入buffer pool,然后再返回。
  2. 执行器拿到引擎给的行数据,把这个值加上 1,比如原来是 N,现在就是 N+1,得到新的一行数据,再调用引擎接口写入这行新数据。
  3. InnoDB引擎记录该行数据的undo log, 然后新数据更新到内存中,如果数据本来就在内存中,则直接修改数据页,如果不再内存中,则将修改记录在change buffer 中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。
  4. 然后告知执行器执行完成了,随时可以提交事务。执行器生成这个操作的 binlog,并把 binlog 写入磁盘。
  5. 执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成。根据 innodb_flush_log_at_trx_commit 决定redo log 是否持久化到磁盘
  6. buffer pool 中对数据页的更新 ,等待脏页刷线操作持久化到磁盘

14.prepare阶段,将事务的xid写入,将binlog_cache里的进行flush以及sync操作(大事务的话这步非常耗时)
15.commit阶段,由于之前该事务产生的redo log已经sync到磁盘了。所以这步只是在redo log里标记commit

4 崩溃恢复的逻辑

崩溃恢复过程中,InnoDB 会从最近的 checkpoint LSN开始,应用 redo log 中的更改,直到达到崩溃时的 write pos LSN,以此来恢复数据库到最后一次提交的状态。

看一下崩溃恢复时的判断规则

  1. 如果 redo log 里面的事务是完整的,则直接提交;
  2. 如果 redo log 里面的事务只有完整的 prepare,则判断对应的事务 binlog 是否存在并完整:
    1. 如果完整,则提交事务;
    2. 否则,回滚事务。==此处事务回滚基于undo log ==
  3. 如果redo log 没有完整的prepare, 则事务基于undo log 回滚

⚠️说明一下,innodb_flush_log_at_trx_commit 实际上控制了redo prepare 和commit 两个阶段的刷盘策略,比如innodb_flush_log_at_trx_commit =1 时在 prepare 阶段和 commit 阶段,redo log 都会持久化写入磁盘。所以才会出现第二种磁盘有且只有完整prepare 的情况。

接下来根据一些具体的问题来详细说明崩溃恢复时的细节

4.1 如何判断 redo log 是完整的

redo log commit 阶段会有commit 标识

4.2. 如果判断binlog 完整性

一个事务的 binlog 是有完整格式的:
statement 格式的 binlog,最后会有 COMMIT;
row 格式的 binlog,最后会有一个 XID event。

4.3. redo log 和 binlog 是怎么关联起来的

在崩溃恢复时,通过读取Redo Log中的Xid,能够将其与Binlog中的Xid进行匹配。

XID(Transaction Identifier) 可以理解成时MySQL server 层的事务唯一标识。
redo log 中会记录XID

如果碰到只有 parepare、而没有 commit 的 redo log,就拿着 XID 去 binlog 找对应的事务。

4.4. 为什么要用2PC 协调binlog和redo log

类似的问题还有,为什么处于 prepare 阶段的 redo log 加上完整 binlog 就可以提交事务。

这两个问题本质上都是数据一致性的问题。

binlog 是server 层日志, 是MySQL 一开始就有的功能,被用在了很多地方,比如备份、主备同步复制。redo log 是InnoDB 层日志,是InnoDB 为了实现事务功能新增的。使用2PC可以维护两份之间的逻辑一致。

那么,为什么要维护两份日志间的逻辑一致呢。

binlog 是server 层日志, 是MySQL 一开始就有的功能,被用在了很多地方,比如备份、主备同步复制。redo log 是InnoDB 层日志,是InnoDB 为了实现事务功能新增的。如果两份日志逻辑或者说数据不一致, 那么用日志恢复出来的数据库状态就有可能和它本来应该的状态不一致。

具体举例来讲,如果不用2PC,两种日志要么是先写 redo log 再写 binlog,或者先写binlog 再写redo log 。
仍然用前面的 update 语句来做例子。假设当前 ID=2 的行,字段 c 的值是 0,再假设执行 update 语句过程中在写完第一个日志后,第二个日志还没有写完期间发生了 crash,会出现什么情况呢?

  1. 先写 redo log 后写 binlog。假设在 redo log 写完,binlog 还没有写完的时候,MySQL 进程异常重启。由于我们前面说过的,redo log 写完之后,系统即使崩溃,仍然能够把数据恢复回来,所以恢复后这一行 c 的值是 1。但是由于 binlog 没写完就 crash 了,这时候 binlog 里面就没有记录这个语句。因此,之后备份日志的时候,存起来的 binlog 里面就没有这条语句。然后你会发现,如果需要用这个 binlog 来恢复临时库的话,由于这个语句的 binlog 丢失,这个临时库就会少了这一次更新,恢复出来的这一行 c 的值就是 0,与原库的值不同。

  2. 先写 binlog 后写 redo log。如果在 binlog 写完之后 crash,由于 redo log 还没写,崩溃恢复以后这个事务无效,所以这一行 c 的值是 0。但是 binlog 里面已经记录了“把 c 从 0 改成 1”这个日志。所以,在之后用 binlog 来恢复的时候就多了一个事务出来,恢复出来的这一行 c 的值就是 1,与原库的值不同。可以看到,如果不使用“两阶段提交”,那么数据库的状态就有可能和用它的日志恢复出来的库的状态不一致。

同理,为什么处于 prepare 阶段的 redo log 加上完整 binlog 就可以提交事务。因为如果binlog 写完以后 MySQL 发生崩溃,这时候 binlog 已经写入了,之后就会被从库(或者用这个 binlog 恢复出来的库)使用。如果redo log 事务不提交的话,就会发生数据不一致的情况

4.5. 不要binlog 可以吗

仅从事务持久化/崩溃恢复这个功能来讲, 只要redo log 是可以完成的。
但是binlog 作为 MySQL 一开始就有的功能,被用在了很多地方,有redo log 无法替代的功能 。

  1. 归档。redo log 是循环写,写到末尾是要回到开头继续写的。这样历史日志没法保留,redo log 也就起不到归档的作用。
  2. 主从复制同步
  3. MySQL 高可用
  4. 在一些业务场景中, 也会使用binlog做数据同步,比如使用canal 同步binlog数据 到ES

    4.6 数据一定不会丢失吗-双1 设置

在介绍binlog和redo log 写入过程的时候,有两个参数
sync_binlog 控制binlog 持久化到磁盘的频率

  1. sync_binlog=0 的时候,表示每次提交事务都不主动刷新磁盘,由文件系统自己控制刷盘频率
  2. sync_binlog=1 的时候,表示每次提交事务都会将 binlog cache 中的内容刷新到磁盘
  3. sync_binlog=N(N>1) 的时候,表示累积 N 个提交事务后才将多个binlog cache中的内容刷新到磁盘。

innodb_flush_log_at_trx_commit 控制redo log 持久化到磁盘的频率

  1. 设置为 0 的时候,表示每次事务提交时都只是把 redo log 留在 redo log buffer 中 ;
  2. 设置为 1 的时候,表示每次事务提交时都将 redo log 直接持久化到磁盘;
  3. 设置为 2 的时候,表示每次事务提交时都只是把 redo log 写到 page cache。

可以看到吗,只有在双1设置的时候,sync_binlog 和 innodb_flush_log_at_trx_commit 都设置成 1, 才能确保一定不会丢数据

通常我们说 MySQL 的“双 1”配置,指的就是 sync_binlog 和 innodb_flush_log_at_trx_commit 都设置成 1。也就是说,一个事务完整提交前,需要等待两次刷盘,一次是 redo log(prepare 阶段),一次是 binlog。

如果不设置成双1, 有助于提高性能。

5. binlog vs redo log

差异

  • 层级差异:Binlog 工作在 MySQL 服务器层,所有引擎都可以使用;而 redo log 是 InnoDB 存储引擎层特有的。
  • 记录形式:Binlog 可以记录 SQL 语句或行变更,redo log 记录的是数据页的物理变化,即“在某个数据页上做了什么修改”
  • 目的和用途:Binlog 主要用于数据复制和崩溃恢复,而 redo log 主要用于事务的持久性和崩溃恢复。
  • 大小管理:Redo log 的大小是固定的,循环使用循环写;binlog 是追加写,可以不断增长,需要定期进行清理。
  • 日志写入:每个线程都拥有自己一块独立的 binlog cache , 而 redo log buffer 是全局共用的

共同点

  • 事务安全:两者都是为了保证事务的持久性和原子性。
  • 恢复支持:在系统或硬件故障后,两者都能被用来恢复数据。
  • 写前日志:都采用了写前日志(write-ahead logging, WAL)的技术,即在实际修改数据库内容前先记录日志。
  • 从生产到写入磁盘均有内存page - 到page cache - 磁盘,刷新到磁盘的时机均有参数控制

相关文章

Intro to 事务
Intro to InnoDB 事务
InnoDB事务-原子性的实现,undo log
InnoDB事务-隔离性的实现,MVCC & 锁