十一、MySQL事务日志-redo log
- 11.1 Buffer Pool
- 11.1.1 缓存的重要性
- 11.1.2 InnoDB 的 Buffer Pool
- 11.1.3 InnoDB 存储引擎线程
- 11.2 redo 日志引入
- 11.3 redo 日志的好处和特点
- 11.3.1 好处
- 11.3.2 特点
- 11.4 redo 日志的组成
- 11.5 redo 日志的整体流程
- 11.6 redo 日志的刷盘策略
- 11.7 redo 日志不同刷盘策略举例
- 01、提交事务时不操作
- 02、提交事务时刷盘
- 03、提交事务后只写入日志文件
- 04 不同刷盘策略的速度和安全性
- 11.8 写入 redo log buffer 过程
- 01、Mini-Transaction
- 02、redo 写入 log buffer
- 03、redo log block 结构
- 11.9 写入 redo log file 过程
- 01、相关参数设置
- 02、日志文件组
- 04、checkpoint 引入
- 05、checkpoint
- 06、checkpoint 举例
- 07、刷新时机(了解)
11.1 Buffer Pool
11.1.1 缓存的重要性
对于使用 InnoDB 存储引擎的表来说,无论是用于存储用户数据的索引,还是各种系统数据,都是以【页】的形式存放在表空间中的。所谓的表空间,只不过是 InnoDB 对于一个或几个实际文件的抽象,但其实,这些数据说到底还是存储在磁盘上的。
但是,磁盘的速度是很慢的。所以 InnoDB 存储引擎在处理客户端的请求时,如果需要访问某个页的数据,就会先把完整的页中的数据全部加载到内存中。即使只需要访问一个页的一条记录,也需要把整个页的数据先加载到内存中。
这样在后续的读写访问后,也并不着急把该页对应的内存空间释放掉,而是将其缓存起来,这样将来有请求再次访问该页面时,就可以省下磁盘 I/O 的开销了。
11.1.2 InnoDB 的 Buffer Pool
为了缓存磁盘中的页,在 MySQL 服务器启动的时候就向操作系统申请了一片连续的内存,这片内存就叫做 Buffer Pool(缓冲池),默认情况下,Buffer Pool 只有128 MB。Buffer Pool 对应的一片连续的内存被划分为若干个页面(缓冲页),页面大小与 InnoDB 表空间的页面大小一致,默认都是 16KB。为了更好地管理这些缓冲页,给每个缓冲页都创建了一些控制信息,并把每个页对应的控制信息占用的内存称为一个控制块,控制块与缓冲页是一一对应的。
每个控制块都对应一个缓冲页,在分配足够多的控制块和缓冲页后,剩余的那点空间可能不够一对控制块和缓冲页的大小了,那么就称这个内存空间为碎片。当然,如果把 Buffer Pool 的大小设置的刚刚好,也可能不产生碎片。
11.1.3 InnoDB 存储引擎线程
InnoDB 存储引擎是多线程的模型,也就是说它拥有多个不同的后台线程,负责处理不同的任务。下面是几种不同的后台线程:
- Master Thread:主要负责将缓冲池中的数据异步刷新到磁盘,保证数据的一致性。
- IO Thread:在 InnoDB 存储引擎中大量使用了 AIO(Async IO)来处理写 IO 请求,这样可以极大提高数据库的性能。IO Thread 的工作主要是负责这些 IO 请求的回调处理。
- Purge Thread:回收已经使用并分配的 undo 页。
- Page Cleaner Thread:将之前版本中脏页的刷新操作都放入到单独的线程中来完成。其目的是为了减轻原 Master Thread 的工作及对于用户查询线程的阻塞,进一步提高 InnoDB 存储引擎的性能。
Master Thread 每秒操作:
- redo日志缓冲刷新到磁盘,即使这个事务还没有提交(总是):即使某个事务还没有提交,InnoDB 仍然每秒会将重做日志缓冲中的内容刷新到重做日志文件。这一点是必须要知道的,因为这可以很好地解释为什么再大的事务提交(commit)的时间也是很短的。
- 合并插入缓冲(可能):合并插入缓冲(Insert Buffer)并不是每秒都会发生的。InnoDB 会判断当前一秒内发生的 IO 次数是否小于 5 次,如果小于 5 次,InnoDB 认为当前的 IO 压力很小,可以执行合并插入缓冲的操作。
- 至多刷新 100 个 InnoDB 的缓冲池中的脏页到磁盘(可能):刷新 100 个脏页也不是每秒都会发生的。InnoDB 通过判断当前缓冲池脏页的比例(buf_get_modified_ratio_pct)是否超过了配置文件中innodb_max_dirty_pages_pct 这个参数(默认为90,代表90%),如果超过了这个阈值,InnoDB 认为需要做磁盘同步操作,将100个脏页写入磁盘。
- 如果当前没有用户活动,则切换到background loop(可能)
11.2 redo 日志引入
为什么需要 redo 日志?
内存中的缓冲池里的数据是定期刷新到磁盘的,并不是每次变更都会实时刷盘。
如果一个事务提交了,刚写进内存,还没来得及刷盘,这时数据库宕机了,那么这段数据就丢失了,后续也无法恢复。从另一方面看,事务的持久性要求事务一旦提交,即使数据库崩溃,该事务对数据库的操作也不能丢失。
如何保证这个持久性呢?一个简单的做法就是:在事务提交完成之前把该事务所修改的所有页面都刷新到磁盘,但是这个简单粗暴的做法有些问题:
- 修改量与刷新磁盘工作量严重不成比例(每次改动无论改动多少都要刷新一整个页面,即使改动的点非常小)。
- 随机 IO 刷新比较慢。
InnoDB 引擎的事务采用了 WAL 技术(Write-Ahead Logging 日志优先),即:先写日志再写磁盘
。只有日志写入成功才算事务提交成功,这里的日志指的就是 redo log。当发生宕机且数据未写入磁盘时,可以通过 redo log 来恢复,保证 ACID 中的 D。
例如:某个事务将系统表空间中的第 10 号页面,偏移量为 100 处的字节由 1 改为 2,这时只需要记录一下 “将 0 号表空间的第10 号页面的偏移量为 100 处的值更新为 2”。
11.3 redo 日志的好处和特点
11.3.1 好处
-
redo 日志降低了刷盘频率
-
redo 日志空间占用很小
存储了表空间 ID、页号、偏移量以及需要更新的值,索引存储空间很小。
11.3.2 特点
-
redo 日志是顺序写入磁盘
在执行事务的过程中,每执行一条语句,就可能产生若干条 redo 记录,这些日志按照产生的顺序依次写入磁盘,即顺序 IO,效率比随机 IO快。
-
事务执行过程中,redo log 不断记录
redo 日志是存储引擎层产生的,而 bin-log 是 Server 层产生。假设一个事务,对表进行批量插入,在这个过程中会一直不断的向 redo 日志中顺序写入数据,而 bin-log 不会记录,直到这个事务提交时,才会一次性写入 bin-log 文件中。
11.4 redo 日志的组成
redo log 由两部分组成:
- redo log buffer 日志缓存:重做日志缓存。存在于内存中,容易发生丢失。
- redo log file 日志文件:重做日志文件。存在于磁盘中,不容易丢失。
在服务启动时候就申请了一段名为 redo log buffer 的连续内存空间,这片内存空间被划分为若干个连续的 redo log block。一个 block 的大小为 512 字节。
参数设置:innodb_log_buffer_size 默认值16M
mysql> show variables like 'innodb_log_buffer_size';
+------------------------+----------+
| Variable_name | Value |
+------------------------+----------+
| innodb_log_buffer_size | 16777216 |
+------------------------+----------+
1 row in set, 1 warning (0.00 sec)
11.5 redo 日志的整体流程
以一个更新事务为例 ,redo log 流转过程如图所示:
- 将磁盘的原始数据加载进内存,修改内存中的数据;
- 生成一条 redo log,并写入到 redo log buffer 中,记录的是数据被修改后的值;
- 当事务 commit 时,将 redo log buffer 中的内容按照追加的方式将数据刷新到 redo log file 中;
- 定期将内存中修改的数据刷新到磁盘中。
Write-Ahead Log(预先日志持久化):在持久化一个数据页之前,先将内存中相应的日志页持久化。
11.6 redo 日志的刷盘策略
redo log 的写入并不是直接写入磁盘的,InnoDB 存储引擎会在写 redo log 的时候先写入到 redo log buffer,之后以一定的频率刷入到真正的 redo log file 中。其中,一定的频率就是指刷盘策略
。
这里需要注意,redo log 由 buffer 刷新到 redo log file,并不是真正的刷新到磁盘,只是刷新到文件系统缓存(page cache)
中去(现代操作系统提高文件写入效率做的优化),而什么时候真正的写入则由操作系统自己决定。
那么对于 InnoDB 就会存在一个问题:如果系统宕机,那么 page cache 中的数据也会丢失,造成 redo log file 写入失败,数据也就丢失了(尽管这种情况的概率是非常小的)。
针对这种情况,InnoDB 给出三种策略,通过 innodb_flush_log_at_trx_commit 参数控制在提交事务时,如何刷新 redo log 到 redo log file 中:
- 设置为 0:每次
提交事务时不进行刷盘操作
(系统默认 master thread 每间隔一秒进行一次同步)。 - 设置为 1:每次
提交事务时把 redo log buffer 内容写入到日志文件中,并将日志文件中的数据更新到磁盘
(默认值)。 - 设置为 2:每次
提交事务时只把 redo log buffer 内容写入到日志文件中
。每秒钟将日志文件中的数据更新到磁盘一次,该操作由操作系统调度。
mysql> show variables like 'innodb_flush_log_at_trx_commit';
+--------------------------------+-------+
| Variable_name | Value |
+--------------------------------+-------+
| innodb_flush_log_at_trx_commit | 1 |
+--------------------------------+-------+
1 row in set, 1 warning (0.00 sec)
另外,在 InnoDB 引擎中有一个后台线程,每隔一秒就会把 redo buffer log 中的内容写入到文件系统缓存 page buffer,然后调用刷盘操作。
也就是说,一个没有提交事务的 redo log 记录,也可能会刷盘。因为在事务执行过程 redo log 记录是会写入 redo log buffer 中,这些 redo log 记录会被后台线程刷盘。
除了后台线程每秒 1 次的轮询操作,还有一种情况,当 redo log buffer 占用的空间即将达到 innodb_flush_log_at_trx_commit(默认是 16M)的一半的时候,后台线程会主动刷盘。
11.7 redo 日志不同刷盘策略举例
01、提交事务时不操作
innodb_flush_log_at_trx_commit 0
异步刷盘:日志缓存区将每隔一秒写到日志文件中,并且将日志文件的数据刷新到磁盘上。该模式下在事务提交时不会主动触发写入磁盘的操作。
除了后台线程每秒 1 次的轮询操作,还有当 redo log buffer占用的空间即将达到innodb_log_buffer_size (这个参数默认是 16M) 的一半的时候,后台线程会主动刷盘。
该模式下一个事务如果还没有提交,但产生的 redo log 记录也有可能被刷盘,这部分 redo log buffer 中的数据会被后台线程刷新到磁盘。
02、提交事务时刷盘
innodb_flush_log_at_trx_commit 1
主动刷盘:每次事务提交时 MySQL 都会把日志缓存区的数据写入日志文件中,并且刷新到磁盘中,该模式为系统默认。
- 当 innodb_flush_log_at_trx_commit 值为 1 时,只要事务提交成功,redo log 记录就一定在硬盘里,不会有任何数据丢失。
- 如果事务执行期间 MySQL 服务宕机,若事务未提交,则不影响数据。
- 此方式可以保证事务 ACID 中的 D,但是是效率最差的。
- 建议使用默认值。即使操作系统宕机的概率小于数据库宕机的概率,既然使用了事务,那么数据的安全相对来说更重要些。
03、提交事务后只写入日志文件
innodb_flush_log_at_trx_commit 2
操作系统决定刷盘:每次提交事务时只把 redo log buffer 内容写入到 page cache 中,但不进行刷盘。该模式下,MySQL 会每秒将日志文件中的数据更新到磁盘执行一次刷盘操作。
也就是说,该模式下 redo log 写入缓存不受参数 innodb_flush_log_at_trx_commit 的影响,因为后台会实时将记录写入缓存。
04 不同刷盘策略的速度和安全性
- 当设置为 0,该模式速度最快,但不太安全,mysqld 进程的崩溃会导致上一秒钟所有事务数据的丢失;
- 当设置为 1,该模式是最安全的,但也是最慢的一种方式。在 mysqld 服务崩溃或者服务器主机宕机的情况下,日志缓存区只有可能丢失最多一个语句或者一个事务;
- 当设置为 2,该模式速度较快,较取值为 0 情况下更安全,只有在操作系统崩溃或者系统断电的情况下,上一秒钟所有事务数据才可能丢失。
11.8 写入 redo log buffer 过程
01、Mini-Transaction
MySQL 把对底层页面的一次原子访问的过程称为一个 Mini-Transaction,简称 mtr
。比如向某个索引的 B+Tree 中插入一条记录的过程就是一个 mtr,一个 mtr 包含一组 redo log,在进行崩溃恢复时,这组日志是一个不可分割的整体。
一个事务可以包含若干条语句,一个语句包含若干个 mtr,每个 mtr 又可以包含若干条 redo log:
02、redo 写入 log buffer
向 log buffer 中写入 redo log 的过程是顺序的
,也就是先往前面的 block 中写,当该 block 的空闲空间用完之后再往下一个 block 中写。当我们想往 log buffer 中写入 redo log 时,第一个遇到的问题就是应该写在哪个 block 的哪个偏移量处,所以 InnoDB 的设计者特意提供了一个称之为 buf_free 的全局变量,该变量指明后续写入的 redo log 应该写入到 log buffer 中的哪个位置:
其中,一个 block 的大小时 512字节。
一个 mtr 执行过程中可能产生若干条 redo log,这些 redo log 是一个不可分割的组(在 log buffer 中必须存在在一起),所以其实并不是每生成一条 redo log,就将其插入到 log buffer 中,而是每个 mtr 运行过程中产生的日志先暂时存到一个地方,当该 mtr 结束的时候,将过程中产生的一组 redo log 再全部复制到 log buffer 中。
假设现在有两个名为 T1、T2 的事务,每个事务都包含 2 个 mtr,给这几个 mtr 命名一下:
- 事务 T1 的两个 mtr 分别称为 mtr_T1_1 和 mtr_T1_2
- 事务 T2 的两个 mtr 分别称为 mtr_T2_1 和 mtr_T2_2
每个 mtr 都会产生一组 redo log:
不同的事务可能是并发执行的,所以 T1、T2 事务的 mtr 可能是交替执行存储的,一个 mtr 执行完,伴随着一组 redo log 就会被复制到 log buffer 中:
其中,有的 mtr 产生的 redo log 量非常大,比如 mtr_t1_2 产生的 redo log 占用的空间就比较大,占用了 3 个 block 来存储。
03、redo log block 结构
一个 redo log buffer 是由日志头、日志体、日志尾
组成。日志头占用 12 字节,日志尾占用 8 字节,所以一个 block 真正能存储的数据也就是 512-12-8 = 492 字节。
为什么一个 block 设计成 512 字节?这个和磁盘的扇区有关,机械磁盘默认的扇区就是 512 字节,如果要写入的数据大于 512 字节,那么要写入的扇区肯定不止一个,这时就要设计到盘片的转动,找到下一个扇区,假设需要写入两个扇区 A 和 B,如果扇区 A 写入成功,而扇区 B 写入失败,那么就会出现非原子性的写入,而如果每次只写入和扇区的大小一样的 512 字节,那么每次的写入都是原子性的。
11.9 写入 redo log file 过程
01、相关参数设置
-
innodb_log_file_size:用于设定 MySQL 日志组中每个日志文件的大小。
mysql> show variables like 'innodb_log_file_size'; +----------------------+----------+ | Variable_name | Value | +----------------------+----------+ | innodb_log_file_size | 50331648 | +----------------------+----------+ 1 row in set, 1 warning (0.01 sec)
-
innodb_log_files_in_group:指定日志组个数。默认为 2 个日志组。
mysql> show variables like 'innodb_log_files_in_group'; +---------------------------+-------+ | Variable_name | Value | +---------------------------+-------+ | innodb_log_files_in_group | 2 | +---------------------------+-------+ 1 row in set, 1 warning (0.00 sec)
-
innodb_log_group_home_dir:redo log 日志文件存储位置,默认 ./ 当前数据目录。
mysql> show variables like 'innodb_log_group_home_dir'; +---------------------------+-------+ | Variable_name | Value | +---------------------------+-------+ | innodb_log_group_home_dir | .\ | +---------------------------+-------+ 1 row in set, 1 warning (0.00 sec)
-
innodb_log_buffer_size: redo log 缓存大小默认 16M。
mysql> show variables like 'innodb_log_buffer_size'; +------------------------+----------+ | Variable_name | Value | +------------------------+----------+ | innodb_log_buffer_size | 16777216 | +------------------------+----------+ 1 row in set, 1 warning (0.00 sec)
02、日志文件组
磁盘上的 redo log 文件不止一个,而是以一个日志文件组的形式出现的。这些文件以 ib_logfilr[数字] (数字可以是 0、1、2)的形式进行命名,每个 redo log 文件大小都是一样的。在将 redo log 写入文件组时,是以 ib_logfile0 开始写,如果 ib_logfile0 写满了,就接着 ib_logfile1 写。同理,ib_logfile1 写满了就去写 ib_logfile2,依次类推。如果写到最后一个文件该怎么办呢?那就重新转到 ib_logfile0 继续写,整个过程如下:
总共的 redo log 文件大小其实就是:innodb_log_file_size ✖ innodb_log_files_in_group。
这种采用循环写的方式,会覆盖掉前面的文件内容,所以就提出了 checkpoint 的概念。
04、checkpoint 引入
有了 redo log,我们仍然会面临这样 3 个问题:
- 缓冲池 buffer pool 不是无限大的,也就是说,不能一直不断地存储数据,然后等待一起刷新到磁盘;
- redo log 是循环使用而不是无限大(也有这个可能 ,但是成本太高,同时不便于运维),那么当所有的 redo log file 都写满了怎么办?
- 当数据库运行了几个月甚至几年时,一旦发生宕机,redo log 不做处理的话会非常大,重新应用 redo log 的时间会非常久,恢复的代价也会非常大。
所以,引入了 checkpoint:
- 缓冲池不够用时,将脏页刷新到磁盘:所谓缓冲池不够用的意思就是缓冲池的空间无法存放新读取到的页,这个时候 InnoDB 引擎会怎么办呢?LRU 算法。InnoDB 存储引擎对传统的 LRU 算法做了一些优化,用其来管理缓冲池这块空间。
- redo log 不可用时,将脏页刷新到磁盘:文件组写满了,需要推进 checkpoint,擦除部分数据。
- 缩短数据库的恢复时间:当数据库发生宕机时,数据库不需要重做所有的日志,因为 checkpoint 之前的页都已经刷新回磁盘。所以数据库只需对 checkpoint 后的 redo log 进行恢复就行了,这显然大大缩短了恢复的时间。
一句话概括就是:checkpoint 其实就是在 redo log file 中找到一个位置,将这个位置的页都从 buffer pool 中刷新到磁盘中去,这个位置就成为 checkpoint(检查点)。
05、checkpoint
在整个日志文件组中有两个重要的属性,分别是 write-ops、checkpoint:
- write pos:当前记录的位置,一边写一边往后记录。
- checkpoint:当前可以擦除的位置,也是不断往后移动的。
当所有的 redo log file 都写满了,无法写入新的数据时,就需要清理掉无用的 redo log 了。
小贴士:redo log 中的数据并不是时时刻刻都是有用的,那些已经不再需要的部分就称为 “可以被重用的部分”,即当数据库发生宕机时,数据库恢复操作不需要这部分的 redo log,因此这部分就可以被覆盖重用(或者说被擦除)。
每次刷盘 redo log 记录到日志文件组中 ,write pos 位置就会后移更新。每次 MySQL 加载日志文件组恢复数据时,会清空加载过的 redo log 记录,并把 checkpoint 后移更新,wriite pos 和 checkpoint 之间的还空着的部分可以用来写入新的 redo log 记录。
如果 write pos 追上 checkpoint,表示日志文件组满了 ,这时就不能再写入新的 redo log 记录了,MySQL 得停下来,清空一些记录,把 checkpoint 推进一下。
06、checkpoint 举例
举个具体的例子解释一下:一组 4 个文件 ,每个文件大小都是 1GB,那么总共有 4GB 的 redo log file 空间。
-
write pos 是当前 redo log 记录的位置,随着不断地写入磁盘,write pos 也不断地往后移,写到 file3 末尾后就回到 file0 开头。
-
checkpoint 是要擦除的位置(将 checkpoint 之前的页刷新到磁盘),也是往后推移并且循环的。
write pos 和 checkpoint 之间的就是 redo log file 上还空着的部分,可以用来记录新的操作。如果 write pos 追上 checkpoint,就表示 redo log file 满了,这时不能再执行新的更新,得停下来先覆盖 (擦掉)一些 redo log 把 checkpoint 推进一下。
07、刷新时机(了解)
在 InnoDB 存储引擎内部,有两种 checkpoint,分别为:
-
Sharp Checkpoint:发生在数据库关闭时将所有的脏页都刷新回磁盘,这是默认的工作方式,即参数 innodb_fast_shutdown=1 (所有的脏页)。
-
Fuzzy Checkpoint:发生在运行时。如果数据库在运行时也使用 Sharp Checkpoint,那么数据库的可用性就会受到很大的影响。故在 InnoDB 存储引擎内部使用 Fuzzy Checkpoint 进行页的刷新,即只刷新一部分脏页,而不是刷新所有的脏页回磁盘。(一部分脏页)
-
Master Thread Checkpoint
对于 Master Thread 中发生的 Checkpoint,差不多以每秒或每十秒的速度从缓冲池的脏页列表中刷新一定比例的页回磁盘。这个过程是异步的,即此时 InnoDB存储引擎可以进行其他的操作,用户查询线程不会阻塞。
-
LUSH LRU LIST Checkpoint
FLUSH LRU_LIST Checkpoint 是因为 InnoDB 存储引擎需要保证 LRU 列表中需要有差不多 100 个空闲页可供使用,可以通过参数 innodb_lru_scan_depth 控制 LRU 列表中可用页的数量,该值默认为1024,buffer pool 中 LRU 列表剩余空间低于 innodb_lru_scan_depth,移除列表尾端页时,若是脏页则触发 checkpoint。
-
Async/Sync Flush Checkpoint
Async/Sync Flush Checkpoint 指的是重做日志文件不可用的情况,这时需要强制将一些页刷新回磁盘,而此时脏页是从脏页列表中选取的。
-
Dirty Page too much Checkpoint
Dirty Page too much,即脏页的数量太多导致 InnoDB 存储引擎强制进行 Checkpoint。其目的总的来说还是为了保证缓冲池中有足够可用的页。其可由参数 innodb_maxdirty_pages_pct 控制。