MySQL 事务日志
事务的 4 种特性:原子性、一致性、隔离性和持久性。
- 事务的隔离性是由锁机制实现
- 事务的原子性、一致性和持久性由事务的 redo 日志和 undo 日志来保证。
- REDO LOG 称为
重做日志
,提供写入操作,恢复提交事务修改的页操作,用来保证事务的持久性 - UNDO LOG 称为
回滚日志
,回滚行记录到某个特定版本,用来保证事务的原子性、一致性
- REDO 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 的作用
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_logfile0
和 ib_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 中。
举例:
有两个事务 名为:T1
、T2
。每个事务都有两个 mtr
- 事务
T1
的两个mtr
分别为:mtr_T1_1
和mtr_T1_2
- 事务
T2
的两个mtr
分别为:mtr_T2_1
和mtr_T2_2
不同事务可能是 并发
执行,所以 T1
、T2
的 mtr
可能是 交替执行
的。每一个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_logfile0
和ib_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)
。每个回滚段都记录了1024
个 undo 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_segmentsinnodb_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. 回滚段与事务
- 每个事务只会使用一个回滚段,一个回滚段在同一时间可能会服务于多个事务。
- 当一个事务开始时,会指定一个回滚段,在事务进行的过程中,当数据被修改时,原始的数据会被复制到回滚段。
- 在回滚段中,事务会不断填充盘区,知道事务结束或者所有空间被耗尽。若当前盘区不够用,事务会在段中请求扩展下一个盘区,若所有被分配的盘区都被用完,则会覆盖最初的盘区或者在回滚段允许的情况下扩展新的盘区使用
- 回滚段存在 undo 表空间中,在数据库中可以存在多个 undo 表空间,但同一时刻只能使用一个 undo 表空间。
mysql> show variables like 'innodb_undo_tablespaces';
+-------------------------+-------+
| Variable_name | Value |
+-------------------------+-------+
| innodb_undo_tablespaces | 2 |
+-------------------------+-------+
- 当事务提交时,InnoDB 存储引擎会:
- 将 undo log 放入列表中,以供之后的 purge 操作
- 判断 undo log 所在页是否可以重用,若可以则分配给下个事务使用
3.回滚段中的数据分类
未提交的回滚数据
:数据所关联的事务未提交已经提交但未过期的回滚数据
:数据所关联的事务已经提交,但是仍受到undo retention 参数的保持时间的影响事务已经提交并过期的数据
:事务已经提交,且数据保存时间已经超过 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 的逆过程