笔记整理自 【宋红康】MySQL数据库(mysql安装/基础/高级/优化),并从《MySQL实战45讲》作为补充

事务有4种特性:原子性、一致性、隔离性和持久性。那么事务的四种特性到底是基于什么机制实现呢?

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

redo日志

如何保证持久性?

1、思路1

在事务提交完成之前把该事务所修改的所有页面都刷新 到磁盘

缺点:

  • 刷新一个完整的数据页太浪费了
  • 随机10刷新起来比较慢。

2、思路2

修改 了哪些东西 记录一下 即可。

redo 日志本质上只是记录了一下事务对数据库进行了哪些修改

比如,某个事务将系统 表空间中 第10号 页面中偏移量为 100 处的那个字节的值 1 改成 2 。我们只需要记录一下:将第0号表 空间的10号页面的偏移量为100处的值更新为 2 。

这样在事务提交时 就会把产述内容刷新到磁盘中。即使之后系统崩溃了,重启之后只要 按照上述内容所记录的步骤重新更新一下数据页 那么该事务对数据库中所做的修改就可以被恢复出来

image-20220128090914931

优点:

  • redo日志降低了刷盘频率

  • redo日志占用的空间非常小

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

redo日志格式

image-20220128095647662

  • type:这条redo日志的类型。
    在MySQL 5.7.22版本中,设计InnoDB的大叔一共为redo日志设计了53种不同的类型。
  • space ID:表空间ID
  • page number:页号。
  • data:这条redo日志的具体内容。

Mini-Transaction

以组的形式写入redo日志

保证原子性的操作时,必须以组的形式来记录 redo 日志。例如向某个索引对应的 插入一条记录的过程必须是原子的 ,不能说插了一半之后就停止了。

如何划分组

  • 一系列日志

    某个需要保证原子性的操作所产生 一系 redo 日志,必须以一条类型为 MLOG_MULTl _REC_END redo 日志结尾,如图所示

    image-20220128094237901

    只有解析到类型为 MLOG_MULTl_REC_END redo 日志时,才认为解析到了一组完整的 redo 才会进行恢复

  • 一条日志

    如果 type 字段的第个比特为1代表这个需要保证原子性的操作只产生了一条单一的 redo 日志

    image-20220128095929621

Mini-Transaction概念

把对底层页面进行一次原子访问的过程称为一个 Mini-Transaction (MTR )。一个MTR可以包含一组redo日志,在进行崩溃恢复时,需要把这一组redo日志作为一个不可分割的整体来处理。

一个事务可以包含若干条语句,每一条语句其实是由若干个 mtr 组成,每一个 mtr 又可以包含若干条 redo日志

image-20220128100213322

redo日志的写入redo buffer过程

redo的组成

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

    参数设置:innodb_log_buffer_size

  • 重做日志文件 (redo log file) ,保存在硬盘中,是持久的。

redo log block

为了更好地管理redo日志,把通过 MTR 生成的日志都放在了大 小为 512 字节的页(block)中。

image-20220128100406670

里面包含的具体字段:

image-20220128100443993

log block header中的属性:

  • LOG_BLOCK_HDR NO:每一个block都有一个大于0的唯一编号,该属性就表示该编号值。
  • LOG_BLOCK_HDR_DATA_LEN:表示block中已经使用了多少字节
  • LOG_BLOCK_FIRST_REC_GROUP:一条redo日志也可以称为一条redo日志记录(redo log record),一个MTR会生成多条redo日志记录,这个MTR生成的这些redo日志记录被称为一个redo日志记录组(redo log record group)
  • LOG_BLOCK_ CHECKPOINT_NO:表示checkpoint的序号;

log block trailer的属性:

  • LOG_BLOCK_CHECKSUM:表示该block的校验值,用于正确性校验

redo 日志写入log buffer

向log buffer中写入redo日志的过程是顺序写入的。使用buf_free全局变量指明后续写入的redo日志应该写到log buffer中的哪个位置

image-20220128103956776

现在假设有名为 T1、T2 的两个事务 每个事务都 包含 MTR,这几个 MTR 名字如下·

  • 事务T1的两个 MTR 分别称为mtr_t1_1和mtr_t1_2

  • 事务T2的两个 MTR 分别称为mtr_t2_1和mtr_t2_2

每个mtr都会产生一组redo日志,用示意图来描述一下这些mtr产生的日志情况:

image-20220128104238915

不同的事务可能是 并发 执行的,所以 T1 、 T2 之间的 MTR 可能是 交替执行 的。每当 MTR 执行完成时,伴随该生成的一组日志就需要被复制到 log uffe 中。

image-20220128104301582

redo log的刷盘策略

redo log的写入并不是直接写入磁盘的,InnoDB引擎会在写redo log的时候先写redo log buffer,之后以 一 定的频率 刷入到真正的redo log file 中。

image-20220128105235439

注意,redo log buffer刷盘到redo log file的过程并不是真正的刷到磁盘中去,只是刷入到 文件系统缓存 (page cache)中去(这是现代操作系统为了提高文件写入效率做的一个优化),真正的写入会交给系 统自己来决定(比如page cache足够大了)。

那么对于InnoDB来说就存在一个问题,如果交给系统来同 步,同样如果系统宕机,那么数据也丢失了(虽然整个系统宕机的概率还是比较小的)。

针对这种情况,InnoDB给出 innodb_flush_log_at_trx_commit 参数,该参数控制 commit提交事务 时,如何将 redo log buffer 中的日志刷新到 redo log file 中。它支持三种策略:

  • 设置为0 :表示每次事务提交时不进行刷盘操作。(系统默认master thread每隔1s进行一次重做日 志的同步)

    image-20220128110220108

  • 设置为1 :表示每次事务提交时都将进行同步,刷盘操作( 默认值 )

    image-20220128110210391

  • 设置为2 :表示每次事务提交时都只把 redo log buffer 内容写入 page cache,不进行同步。由os自 己决定什么时候同步到磁盘文件。

    image-20220128110258733

redo log file

1、 相关参数设置

  • innodb_log_group_home_dir :指定 redo log 文件组所在的路径,默认值为 ./ ,表示在数据库 的数据目录下。
  • innodb_log_files_in_group:指明redo log file的个数,命名方式如:ib_logfile0,iblogfile1… iblogfilen。默认2个,最大100个。
  • innodb_flush_log_at_trx_commit:控制 redo log 刷新到磁盘的策略,默认为1。
  • innodb_log_file_size:单个 redo log 文件设置大小,默认值为 48M 。最大值为512G,注意最大值 指的是整个 redo log 系列文件之和,即(innodb_log_files_in_group * innodb_log_file_size )不能大 于最大值512G。

2、日志文件组

磁盘上的redo日志文件不止一个,而是以一个日志文件组 的形式出现的.这些文件以 ib_logfile[ 数字 ]” (数字可 2…) 的形式进行命名。

在将redo日志写入日志文件组时,从ib_logfile0开始写起;如果ib_logfile0写满了,就接着ib_logfile1写。如果写到最后一个文件,重新转到ib_logfile0继续写

image-20220128110531963

总共的redo日志文件大小其实就是: innodb_log_file_size × innodb_log_files_in_group 。 采用循环使用的方式向redo日志文件组里写数据的话,会导致后写入的redo日志覆盖掉前边写的redo日 志?当然!所以InnoDB的设计者提出了checkpoint的概念。

3、 checkpoint

  • write pos是当前记录的位置,一边写一边后移,写到文件末尾后就回到文件开头
  • checkpoint是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。

image-20220128111116746

如果 write pos 追上 checkpoint ,表示日志文件组满了,这时候不能再写入新的 redo log记录,MySQL 得 停下来,清空一些记录,把 checkpoint 推进一下

image-20220128111131130

redo的整体流程

以一个更新事务为例,redo log 流转过程,如下图所示:

image-20220128105033879

第1步:先将原始数据从磁盘中读入内存中来,修改数据的内存拷贝

第2步:生成一条重做日志并写入redo log buffer,记录的是数据被修改后的值

第3步:当事务commit时,将redo log buffer中的内容刷新到 redo log file,对 redo log file采用追加 写的方式

第4步:定期将内存中修改的数据刷新到磁盘中

这其实就是Write-Ahead机制

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

有了redo log,InnoDB就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为crash-safe

binlog日志

概念

MySQL整体来看,其实就有两块:一块是Server层,它主要做的是MySQL功能层面的事情;还有一块是引擎层,负责存储相关的具体事宜。

redo log是InnoDB引擎特有的日志,而Server层也有自己的日志,称为binlog(归档日志)。

redo log与binlog区别

  1. redo log是InnoDB引擎特有的;binlog是MySQL的Server层实现的,所有引擎都可以使用。
  2. redo log是物理日志,记录的是“在某个数据页上做了什么修改”;binlog是逻辑日志,记录的是这个语句的原始逻辑,比如“给ID=2这一行的c字段加1 ”。
  3. redo log是循环写的,空间固定会用完;binlog是可以追加写入的。“追加写”是指binlog文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。

整体流程分析

接下来我们对更新流程进行分析:

create table T(ID int primary key, c int);

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

image-20220128111735475

将redo log的写入拆成了两个步骤:prepare和commit,这就是"两阶段提交”。

为什么要有两阶段提交?

为了保证数据库的状态就有和用它的日志恢复出来的库的状态一致,简单说,redo log和binlog都可以用于表示事务的提交状态,而两阶段提交就是让这两个状态保持逻辑上的一致。

image-20220128112731343

  • 当在2之前崩溃时
    重启恢复:后发现没有commit,回滚。

    备份恢复:没有binlog 。一致

  • 当在3之前崩溃
    重启恢复:虽没有commit,但满足prepare和binlog完整,所以重启后会自动commit。备份:有binlog。一致