事务的 4 种特性:原子性、一致性、隔离性和持久性。

  • 事务的隔离性是由锁机制实现
  • 事务的原子性、一致性和持久性由事务的 redo 日志和 undo 日志来保证。
    • REDO LOG 称为重做日志,提供写入操作,恢复提交事务修改的页操作,用来保证事务的持久性
    • UNDO LOG 称为回滚日志,回滚行记录到某个特定版本,用来保证事务的原子性、一致性

注意:REDO 和 UNDO 都可以视为是一种恢复操作

  • redo log:是在 存储引擎层(InnoDB)生成的日志,记录的是 物理级别 上的页修改操作,例如:页号xxx、偏移量zzz,写入了 ‘www’ 数据。主要为了保证数据的可靠性。
  • undo log:是存储引擎层(InnoDB) 生成的日志,记录的是逻辑操作 日志,例如对某行数据进行 insert 操作,则undo log 记录的是一条与其相反的 DELETE 操作。主要用于事务的回滚(undo log 记录的是每个修改操作的逆操作) 和 一致性非锁定读(undo log 回滚行记录到某个特定版本 —MVCC,即多版本并发控制)

1.redo 日志

InnoDB 存储引擎都是以 页为单位 存储数据。在访问页前,需要将磁盘上的页缓存到内存中的 Buffer Pool 后才可以访问。所有的变更都必须更新缓冲池中的数据,然后缓冲池中的脏页 会以一定的频率被刷到磁盘中(checkPoint机制)。

1.1 为什么需要 REDO 日志

由于 checkpoint 并不是每次变更时就立刻触发的,而是 master 线程隔一段时间去处理的。所以最坏的情况是:事务提交了以后,刚写入到缓冲池中,数据库宕机了,则该段数据丢失了,无法恢复

事务包含了持久性 的特性,就是对一个已经提交的事务。在事务提交后即使系统出现故障/崩溃,这个事务对数据库的操作的更改不应该丢失。

解决方案:

简单的做法:在事务提交之前把该事务做的修改的所有页面都刷新到磁盘中。但是有以下问题:

  • 修改量和刷新磁盘工作量严重不成比例

    若只修改一个页中的某个字节,但是 InnoDB 存储引擎是以页为单位进行磁盘 I/O 的。若只修改一个字节就要刷新整个页面的数据到磁盘中过于浪费性能

  • 随机 I/O 刷新较慢

    一个事务可能包含很多语句,即使一条语句可能修改需要多页,若一个事务中修改的页并不相邻,则需要将该事务修改的 Buffer Pool 中的页面刷新到磁盘中时,需要进行很多的随机I/O,随机I/O 的速度比 顺序 I/O 要慢

另外一个解决思路:将修改了哪些字节记录到一个文件中(redo 日志)。例如:某个事物将系统表空间中 第10 页中偏移量为 100的字节值改为 2,只需要记录:第 0 号表空间的 10 号页面的偏移量为 100 处的值更新为 2

InnoDB 引擎的事务采用了 WAL 技术(Write-Ahead Logging),即先写日志,再写磁盘,只有日志(redo log)写完,才算事务成功提交。当发生宕机且数据未刷到磁盘时,可以通过 redo log 来恢复,保证 ACID 中的 D,这就是 redo log 的作用

Snipaste_2022-08-01_09-41-51

1.2 REDO 日志的好处、特点

1. 好处

  • redo 日志降低了刷盘频率
  • redo 日志占用的空间很小

存储表空间ID、页号、偏移量及需要更新的数据,所需存储空间小,刷盘快

2. 特点

  • redo 日志是顺序写入磁盘的

redo 日志是按照产生的顺序写入磁盘中的,也就是顺序 I/O,效率比随机 I/O 快

  • 事务执行过程中,redo log 不断记录

redo log 和 bin log 的区别:redo log 是存储引擎层产生的,bin log是 数据库层 产生的。例如:一个事务对表进行 10 w 行的记录插入,在这个过程中,会一直不断的向 redo log 顺序记录,而bin log 不会记录,知道事务提交,才会一次写入到 bin log 中

1.3 redo 的组成

redo log 可以分为两个部分:

  • 重做日志的缓冲(redo log buffer) ,保存在内存中,易丢失

服务器启动时就会申请一片空间名为 redo log buffer 的连续存储空间。被划分为若干个连续的 redo log block。一个 redo log block 占用 512 字节大小

参数设置:innodb_log_buffer_size

redo log buffer 大小,默认为 16M,最大值为 4096M,最小值为 1M

mysql> show variables like '%innodb_log_buffer_size%';
+------------------------+----------+
| Variable_name          | Value    |
+------------------------+----------+
| innodb_log_buffer_size | 16777216 |
+------------------------+----------+
  • 重做日志文件(redo log file),保存在硬盘中

redo log 文件如图所示,其中 ib_logfile0ib_logfile1 即为 redo 日志

1.4 redo的整体流程⭐

第一步:将原始数据从磁盘读取到内存中,修改数据的内存拷贝
第二步:生成一条重做日志并写入 redo log buffer,记录的是数据被修改后的值
第三步:当事务 commit 时,将 redo log buffer 中的内容刷到 redo log file,对 redo log file 采用追加写的方式
第四步:定期将内存中修改的数据刷新到磁盘中

Write-Ahead Log(预先日志持久化):在持久化一个数据页之前,将将内存中相应的日志页持久化

1.5 redo log 的刷盘策略

redo log 的写入是 InnoDB 存储引擎会在写 redo log 时先写 redo log buffer 中,之后再以一定频率写入到磁盘上的 redo log file

注意:redo log buffer 刷盘到磁盘中的 redo log file 的过程中并不是真正的刷到磁盘中,只是刷入到 文件系统缓存(page cache)中(是操作系统为了提高文件写入效率的优化),真正的写入会让系统来决定什么时候写入。对于 InnoDB 来说存在一个问题,若交给了系统来同步,同样若系统宕机了,则数据也丢失了

解决方案:InnoDB 提供了 innodb_flush_log_at_trx_commit 参数,该参数控制 commit 提交事务时,redo log buffer 刷到 redo log file 中的策略:

  • 设置为0:表示每次事务提交不进行刷盘操作。(系统默认会有个线程每个 1s 进行一次将 redo log buffer 写入到 page cache,后调用刷盘操作)
  • 设置为1:表示每次事务提交时都进行同步,也就是刷盘操作(默认值
  • 设置为2:表示每次事务提交时都只是将 redo log buffer 内容写入到 page cache 中,不进行同步,由系统决定什么时候写入到磁盘文件中
show variables like 'innodb_flush_log_at_trx_commit';
+--------------------------------+-------+
| Variable_name                  | Value |
+--------------------------------+-------+
| innodb_flush_log_at_trx_commit | 1     |
+--------------------------------+-------+

另外,InnoDB 存储引擎有一个后台线程,每间隔1 秒,就将 redo log buffer 中的内容写入到文件系统缓存中 (page cache),然后进行刷盘操作

也就是,一个没有 提交事务的 redo log 记录,可能会刷盘。因为在事务的执行过程中 redo log 是会不断写入到 redo log buffer 中的,redo log 记录会被后台线程刷盘

除了后台线程每秒1次的操作,还有一种情况,当redo log buffer 占用的空间即将达到 innodb_log_buffer_size (默认为 16M)的一半时,后台线程会主动刷盘

1.6 不同刷盘策略演示

1.流程图

小结:innodb_flush_log_at_trx_commit=1

只要事务提交成功,redo log 记录就一定在硬盘中,不会有任何数据丢失

若在事务执行过程中 MySQL 出问题或宕机,这部分日志丢失了,但是事务并没有提交,所以尽管日志丢失也不会有影响。可以保证 ACID 中的 D,数据绝对不会丢失,但是效率最差

建议使用默认值

小结:innodb_flush_log_at_trx_commit=2

只要事务提交成功,redo log buffer 中的内容只写入到文件系统缓存(page cache

若是 MySQL 挂了不会有任何数据丢失,若是操作系统宕机可能会有 1 秒数据丢失。无法满足 ACID 的 D。但是效率最高的

小结:innodb_flush_log_at_trx_commit = 0

master thread 每1秒进行一次重做日志的 fsync 操作,则实例 crash 最多丢失 1 秒内的事务(master thread 是负责将缓冲池中的数据异步刷新到磁盘,保证数据的一致性)

数值为 0 ,I/O 效率理论是高于 1 的,低于 2 的。这种策略也有丢失数据的风险,也无法保证 D

虽然可以通过设置 innodb_flush_log_at_trx_commit 为0或2 提高事务提交的性能,但是会失去事务的 ACID 特性

1.7 写入 redo log buffer 的过程

1.Mini-Transaction

MySQL 将对底层页面的一次原子性操作称为:Mini-Transaction ,简称为mtr。例如:向某个索引对应的 B+ 树插入一条数据的过程就是一个 Mini-Transaction 。一个mtr 可以包含一组 redo 日志,在进行崩溃恢复时这一组的 redo 日志作为一个不可分割的整体

一个事务可以包含多条语句,每一条语句都是若干个 mtr组成。每个mtr又可以包含若干条 redo 日志

2. redo 日志写入 log buffer

log buffer 中写入 redo 日志的过程是顺序的,从前往后写,若该 block 的空闲空间使用完后再往下一个 block 中写。如何解决往 log buffer 中写入 redo 日志时,该写入到哪个 block 的哪个偏移量位置,所以 InnoDB 提供了一个名为 buf_free 的全局变量,来表示写入的 redo 日志应该写在 log buffer 的哪个位置

一个 mtr 执行过程中可能会产生多条 redo 日志,这些redo 日志是不可分割的组,并不是每生成一条 redo 日志,就插入到 log buffer 中,而是每个 mtr 运行过程中产生的日志先暂存起来,当该 mtr 结束时,再将一组 redo 日志全部复制到 log buffer 中。

举例:

有两个事务 名为:T1T2。每个事务都有两个 mtr

  • 事务 T1 的两个mtr 分别为:mtr_T1_1mtr_T1_2
  • 事务 T2 的两个 mtr 分别为:mtr_T2_1mtr_T2_2

不同事务可能是 并发执行,所以 T1T2mtr 可能是 交替执行 的。每一个mtr 执行完后,生成的一组 redo 日志被复制到 log buffer中,不同事务的 mtr 可能是交替写入 log buffer 中的。

3. redo log block 的结构图

一个 redo log block 是由 日志头日志体日志尾组成。日志头占用 12 字节,日志尾占用 8 字节,一个 block 可以存储的数据量是 512-12-8 = 492 字节。

为什么一个 block 设计为 512 字节

机械磁盘默认扇区大小就是为 512 字节。若要写入的数据大于 512 字节,则要写入的扇区不止一个,若出现一个扇区写入成功,一个扇区写入失败,则会出现 非原子性 的写入。若每次写入的大小与扇区大小一样的 512 字节,则每次写入操作都是原子性的

1.8 redo log file

1. 相关参数设置

  • innodb_log_group_home_dir:指定 redo log 文件组成的路径,默认值为 ./ ,即数据库的数据目录下。MySQL 的默认数据目录(var/lib/mysql)下默认有两个 名为 ib_logfile0ib_logfile1的文件,log buffer 中的日志默认情况就是写到这两个磁盘文件中

  • innodb_log_files_in_group:指定 redo log file 的个数。默认为2 最大 100

  • innodb_flush_log_at_trx_commit: 控制 redo log 刷盘策略 默认为 1

  • innodb_log_file_size:单个的 redo log 文件大小,默认值为 48M。所有 redo log 的文件大小总和最大值为 512 G(即 innodb_log_files_in_group * innodb_log_file_size) 不能大于最大值 512 G

show variables like 'innodb_log_file_size';
+----------------------+----------+
| Variable_name        | Value    |
+----------------------+----------+
| innodb_log_file_size | 50331648 |
+----------------------+----------+

可以根据业务修改其大小,便于容纳较大的事务。编辑 my.cnf 文件并重启服务器生效

innodb_log_file_size=200M

2. 日志文件组

磁盘上的 redo 日志文件不止一个,而是以一个日志文件组形式出现的。每个 redo 日志文件大小一致

所有的 redo 日志文件大小总和为:innodb_log_file_size * innodb_log_files_in_group

采用循环使用的方式 向 redo 日志文件组中写数据是否会导致后写入的 redo 日志覆盖掉前面写的 redo 日志?用 checkpoint 解决

3. checkpoint

日志文件组中有两个重要的属性:write pos、checkpoint

  • write pos是当前记录的位置,边写边后移

  • checkpoint是当前需要擦除的位置,往后 移

每次刷盘 redo log 数据到日志文件组,write pos 位置就会后移。每次 MySQL 加载日志文件组恢复数据时,都会清空已加载过的 redo log 记录,并将 checkpoint 往后移。wirte pos 和 checkpoint 之间空着的部分可以用于写入 新的 redo log 记录

若 write pos 追上 checkpoint ,表示日志文件组已满,需要清空一些记录,推进 checkpoint

1.9 redo log 小结

存储形式:InnoDB 的更新操作采用 Write Ahead Log(预先日志持久化)策略,即先写日志,再写入磁盘

2. Undo 日志

redo log是事务持久性的保证,而 undo log 是事务原子性的保证。在事务中更新数据的前置操作是要先写入一个 undo log

2.1 如何理解 Undo 日志

事务要保证原子性,若事务执行过程中,出现问题。例如:

  • 出现错误,服务器出现错误,操作系统错误,突然断电
  • 手动 ROLLBACK 语句结束当前事务的执行

以上情况出现,需要将数据改回执行事务之前的样子,该过程称为 回滚

当对一条记录做改动(可以是 insert、delete、update)时,都需要将回滚所需的东西记录下来。例如:

  • 插入一条记录,至少将该记录的主键值记录下来,回滚时根据主键值删除该条记录
  • 删除一条记录,需要将该记录内容保存下来,回滚时再插入到表中
  • 修改一条记录,需要将修改前的旧值记录下来,回滚时再将其更新为旧值即可

为了回滚而记录的内容称为:撤销日志回滚日志(undo log)

注意:查询不会修改数据,所以查询操作,不需要相应的 undo 日志

此外,undo log 会产生 redo log,也就是undo log 的产生会伴随 redo log 的产生,是因为 undo log 需要持久性的保护

2.2 Undo 日志的作用

  • 作用1:回滚数据

undo 是逻辑日志 ,只是将数据库逻辑上恢复到原来的样子。数据结构和页本身在回滚之后可能不相同。

undo 并不是物理意义上的回退到原来的状态

  • 作用2:MVCC

即 InnoDB 存储引擎中 的 MVCC 是通过 undo 完成的。当用户读取一条记录时,若该记录已经被其他事务占用,当前事务可以通过 undo 读取之前的行版本信息,来实现非锁定读取

2.3 undo 的存储结构

1. 回滚段与 undo 页

InnoDB 对 undo log 的管理使用了 段 的方式,也就是回滚段(rollback segment)。每个回滚段都记录了1024undo log segment,而在每个 undo log segment 段中使用了 undo 页的申请

  • InnoDB 1.1 版本之前(不包括1.1 版本),只有一个 rollback segment,所以支持同时在线的事务限制数量为 1024.
  • 从 1.1 版本开始 InnoDB 最大支持128 个rollback segment,所以支持同时在线的事务限制提高到了 128 * 1024
show variables like 'innodb_undo_logs';
+--------------------------+------------+
| Variable_name            | Value      |
+--------------------------+------------+
| innodb_undo_logs         | 128        |
+--------------------------+------------+

InnoDB 1.1 版本支持了 128 个 rollback segment,但这些 rollback segment 都存储在共享表空间 ibdata中。

参数:

  • innodb_undo_directory:设置 rollback segment 文件所在的路径。可以设置为独立表空间。默认值为:“./” ,表示当前 InnoDB 存储引擎的目录
  • innodb_undo_logs:设置 rollback segment 的个数,默认值为 128。在InnoDB 1.2 版本中,该参数用来替换之前版本的参数 innodb_rollback_segments
  • innodb_undo_tablespaces:设置构成 rollback segment 文件数量,使得 rollback segment 可以比较平均地分布在多个文件中。设置该参数后,会在路径innodb_undo_directory 看到 undo 为前缀的文件,该文件就表示 rollback segment 文件

undo 页的重用

当事务提交后,并不会立刻删除 undo 页。因为重用,所以这个 undo 页可能有其他事务的 undo log。undo log 在 commit 后,会被存放到一个链表中,然后判断该 undo 页的使用空间是否小于 3/4,若小于,则表示该 undo 页可以被重写,则不会被回收。其他事务的 undo log 可以记录在当前 undo 页的后面。由于 undo log 是离散的,所以清理时,效率不高。

2. 回滚段与事务

  1. 每个事务只会使用一个回滚段,一个回滚段在同一时间可能会服务于多个事务。
  2. 当一个事务开始时,会指定一个回滚段,在事务进行的过程中,当数据被修改时,原始的数据会被复制到回滚段。
  3. 在回滚段中,事务会不断填充盘区,知道事务结束或者所有空间被耗尽。若当前盘区不够用,事务会在段中请求扩展下一个盘区,若所有被分配的盘区都被用完,则会覆盖最初的盘区或者在回滚段允许的情况下扩展新的盘区使用
  4. 回滚段存在 undo 表空间中,在数据库中可以存在多个 undo 表空间,但同一时刻只能使用一个 undo 表空间。
mysql> show variables like 'innodb_undo_tablespaces';
+-------------------------+-------+
| Variable_name           | Value |
+-------------------------+-------+
| innodb_undo_tablespaces | 2     |
+-------------------------+-------+
  1. 当事务提交时,InnoDB 存储引擎会:
    • 将 undo log 放入列表中,以供之后的 purge 操作
    • 判断 undo log 所在页是否可以重用,若可以则分配给下个事务使用

3.回滚段中的数据分类

  1. 未提交的回滚数据:数据所关联的事务未提交
  2. 已经提交但未过期的回滚数据:数据所关联的事务已经提交,但是仍受到undo retention 参数的保持时间的影响
  3. 事务已经提交并过期的数据:事务已经提交,且数据保存时间已经超过 undo retention 参数指定的时间。当回滚段满后,会优先覆盖这部分数据

事务提交后并不能立刻删除 undo log 及 undo log 所在页,因为其所在页可能有其他事务的 undo log 数据。所以事务提交时会将 undo log 放入一个链表中,是否可以删除 undo log 和 undo log 所在页由 purge 线程判断

2.4 undo 的类型

在 InnoDB 存储引擎中,undo log 分为:

  • insert undo log

    insert undo log 指的是 在 insert 操作中产生的 undo log。因为 insert 操作的记录,只对事务本身可见,对其他事务不可见(事务隔离性的要求),所以 该 undo log 可以在事务提交后直接删除。不需要进行 purge 操作

  • update undo log

    update undo log 记录是对 delete 和 update 操作产生的 undo log。该undo log 可能需要提供 MVCC 机制,所以不可在事务提交时就进行删除。提交时放入 undo log 链表中,等待 purge 线程进行判断,再由其决定是否删除

2.5 undo log 的生命周期

1. 简要生成过程

假设 A=1 和 B=2 ,将A修改为 3,B修改为4

start transaction;
记录 A = 1 到 undo log;
update A = 3;
记录 A = 3 到 undo log;
记录 B = 2 到 undo log;
update B = 4;
记录 B = 4 到 redo log;
将redo log刷入磁盘
commit;
  • 在 1~8 中任意一步系统宕机,事务未提交,该事务就不会对磁盘上的数据做任何影响
  • 在 8~9 之间宕机,恢复后可以选择回滚,也可以选择提交事务,因为此时 redo log 已经持久化
  • 若在 9 之后宕机,内存中的数据还未刷入磁盘,则系统恢复后,可以根据 redo log 将数据刷回到磁盘上。

只有 Buffer Pool 的流程:

有了 Redo log 和 Undo log 之后:

2. 详细生成过程

InnoDB 存储引擎中,每个行记录有几个隐藏列

  • DB_ROW_ID:若没有显式定义主键,且表中没有定义唯一索引,则InnoDB 会自动为表添加一个 row_id 的隐藏列作为主键
  • DB_TRX_ID:每个事务都会分配一个事务ID,当对某条记录发生变化时,则会将该事务的事务ID 写入到 trx_id 中
  • DB_ROLL_PTR:回滚指针,本质上是指向 undo log

当执行 INSERT 时:

begin;
INSERT INTO user(name) VALUES("tom");

当执行UPDATE时:

更新操作会生成 update undo log,并且会分更新主键和不更新主键的情况

UPDATE user SET name = 'Sun' WHERE id = 1;

若现在执行:

UPDATE user SET id = 2 WHERE id = 1;

对于修改主键的操作,会先将原本的数据 deltemark 标识开启,此时并没有真正的删除数据,真正的删除会交给 purge 线程判断,然后再插入一条新的数据,新的数据也会产生undo log,并且undo log 的序号也会递增

4. undo log 的删除

  • 针对于 insert undo log

    insert操作的数据只对该事务本身可见,其他事务不可见。所以该undo log 可以再事务提交后直接删除,不需要进行 purge 操作

  • 针对于 update undo log

    该 undo log 可能需要提供 MVCC 机制,所以不能在事务提交时就删除,提交时放入 undo 链表中,等待 purge 线程进行判断,再决定是否删除

purge 线程两个主要作用:清理 undo 页清除 page 里面带有 Delete_Bit 标识的数据行。在 InnoDB 中,事务中的 Delete 操作实际上并不是真正的删除掉数据行,而是一种 Delete Mark 操作,在行记录上标识 Delete_Bit,而不是删除记录,真正的删除需要 purge 线程完成。

2.6 小结

undo log 是逻辑日志,对事物回滚时,只是将数据库中的数据逻辑恢复到原本的样子

redo log是物理日志,记录的是数据页的物理变化,undo log 不是 redo log 的逆过程

文章作者: 临川
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 临川羡鱼
MySQL MySQL
喜欢就支持一下吧