携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第3天,点击查看活动详情
1.REDO LOG
CHECK POINT机制:事务提交后缓冲池中的脏页会以一定的频率被刷入磁盘
1.1.作用
由于CHECK POINT机制不是每次变更的时候都触发,而是事务提交后再去执行,所以当变更数据都写入缓冲池后,数据库宕机了,那么这段时间的数据就是丢失的,无法恢复。根据事务的持久性特性,对于一个已经提交的事务,在事务提交后对数据的改变是永久性的,不能丢失。
REDO LOG保证了事务的持久性,将事务在日志中修改的内容记录下来,即使事务提交后系统崩溃,也能在重启后通过REDO LOG记录的操作来刷新磁盘。
InnoDB引擎的事务采用了WAL技术(Write-Ahead Logging),这个技术的思想就是先写日志,再写磁盘,只有日志写入成功,才算事务提交成功。当发生宕机且数据未刷到磁盘的时候,可以通过REDO LOG来恢复,以此保证了事务的持久性
1.2.特点
- 占用空间小,刷盘快(不需要整个页面,只需记录更改的内容)
- 直接IO
- 按顺序写入磁盘
执行事务的过程中,每产生一条语句,就产生对应的REDO日志,这些日志是按照产生的顺序写入磁盘
- 事务执行过程中,REDO LOG不断记录
1.3.组成
- 重做日志的缓冲(REDO LOG BUFFER),保存在内存中,容易丢失
在服务器启动时会申请一大片连续的REDO LOG BUFFER内存空间,这片空间被划分为若干个连续的REDO LOG BLOCK,一个BLOCK占用512字节
参数设置:INNODB_LOG_BUFFER_SIZE,默认大小为16M,最大4096M,最小1M
- 重做日志文件(REDO LOG FILE),保存在硬盘中,是持久的
1.4.流程
以一个UPDATE事务为例:
- 先将原始数据从磁盘读入内存,根据事务操作内存中的数据
- 每执行一条语句生成一条重做日志并写入REDO LOG BUFFER
- 事务COMMIT时,将REDO LOG BUFFER的内容刷新到REDO LOG FILE
- 将内存中的数据刷新到磁盘
1.5.刷盘策略
REDO LOG并不是直接写入磁盘,会先写入REDO LOG BUFFER,之后以一定的频率刷入到真正的REDO LOG FILE中。此时从REDO LOG BUFFER刷盘到REDO LOG FILE的过程并不是真正的刷到磁盘中去,而是刷入到文件系统缓存(page cache)中去(操作系统的优化),真正的写入会交给操作系统决定。
针对COMMIT提交事务时,如何将REDO LOG BUFFER中的日志刷入到REDO LOG FILE中去,InnoDB给出INNODB_FLUSH_LOG_AT_TRX_COMMIT参数。支持三种策略:
- 为0:每次事务提交时不进行刷盘操作。
InnoDB中的master thread每隔1s进行一次重做日志的同步,将LOG BUFFER的数据写入文件缓存并同步刷盘。但是这种情况下事务的提交并不会触发后台线程,如果数据库或者操作系统宕机,都有丢失数据的风险。
- 为1(默认):每次提交事务都进行刷盘操作。
每次事务的结束都会触发后台线程将REDO LOG BUFFER中的内容写入文件系统并刷盘。这种情况下只要事务提交了REDO LOG就一定在硬盘里,不会有数据的丢失。
无论数据库或是操作系统宕机都不会丢失任何已提交的数据,但效率是最差的。
- 为2:每次提交事务都只把REDO LOG BUFFER内容写入page cache,但不进行同步。由操作系统决定什么时候同步到磁盘文件
这种情况下数据库宕机并不会造成数据的丢失,但是操作系统宕机后可能会造成数据丢失,无法满足事务的持久性,但是操作效率是较高
1.6.日志写入REDO BUFFER
Mini-Transaction
MySQL把对底层页面的一次原子访问过程称之为一个Mini-Transaction,简称mtr,一个mtr可以包含一组REDO日志,在进行崩溃恢复时这组日志是一个整体
一个事务可以包含多条语句,一条语句可以由多个mtr组成,每一个mtr又可以包含多个redo日志,它们的关系如下图:
- REDO日志写入REDO BUFFER
向REDO BUFFER中写入日志是按顺序的,需要有一个变量BUF_FREE来后序的REDO日志应该写入REDO BUFFER的哪个位置,如下图:
一个mtr执行的过程中可能产生若干条REDO日志,这些日志是一个不可分割的组,在mtr执行的过程中将产生的日志先暂存,当该mtr执行结束,再将这个mtr全部的日志一起复制到LOG BUFFER中。
假设有事务T1和事务T2,T1有两个mtr:mtr_T1_t1和mtr_T1_t2,T2也有两个mtr:mtr_T2_t1和mtr_T2_t2;不同的事务可能是并发执行的,T1和T2写入BUFFER的mtr可能是交替的,如下图:
- REDO LOG BLOCK
一个REDO LOG BLOCK是由日志头、日志体、日志尾组成。日志头占12字节,日志尾占8字节,所以一个BLOCK真正能存储的数据是:512 - 12 - 8 = 492字节
REDO日志都是存储在492字节的日志体中,日志头和日志尾存储的都是管理信息
1.7.REDO LOG FILE
- 相关参数设置
innodb_log_group_home_dir:指定REDO LOG文件组的所在路径,默认值为./,表示在数据库的数据目录下。
innodb_log_files_in_group:指明REDO LOG FILE的个数,默认2个最大100个
innodb_log_file_size:单个LOG FILE文件的大小,默认48M,所有LOG FILE文件和最大值为512G
- 日志文件组
磁盘上的REDO日志文件不止一个,而是一个日志文件组。这些文件以ib_logfile[数字]的形式命名,每个日志文件的大小都是一样的。在将日志写入日志文件组时,先从0号日志文件开始写,写满后再写入1号、2号文件,以此类推。当写到最后一个文件后,再重新转到0号文件开始写,覆盖掉前面的日志,如图示:
全部日志文件的大小:innodb_log_files_in_group*innodb_log_file_size
-
CHECK POINTWRITE POS:记录当前写的位置,边写边后移CHECK POINT:可擦除的位置,往后推移
每次把REDO LOG刷盘到日志文件中,WRITE POS就会后移;每次MySQL加载日志文件组恢复数据时,CHECK POINT就会后移。
如图,WRITE POS顺时针距离CHECK POINT的距离表示可以写入新的日志的空间,另一部分表示MySQL还未用来恢复数据的文件。
如果WRITE POS追上CHECK POINT表示日志文件组满了,这是不能再写入新的REDO LOG记录,数据库需要停下来,清空一些记录,把CHECK POINT前移
2.UNDO LOG
在事务中更新数据的前置操作就是要先写入UNDO LOG
2.1.概述
UNDO LOG用来保证事务的原子性,当发生回滚时,需要将数据改回原来的样子,符合原子性的要求。当对一条记录做改动时(INSERT、DELETE和UPDATE),要把回滚时需要的数据记录下来:
- 插入数据时:至少需要把主键记录下来,回滚时只需把主键删除(对于每个INSERT,执行一个DELETE)
- 删除记录时:要把这条记录中的内容都记录下来,回滚时再把这些内容插入(对于每个DELETE,执行一个INSERT)
- 修改记录:要把这条记录的旧值记录下来,回滚时再更新为旧值(对于每个UPDATE,执行一个相反的UPDATE)
这些为了回滚而记录的内容称之为回滚日志或撤销日志。
UNDO LOG也会产生REDO LOG,也需要持久性的保护。
2.2.作用
- 回滚数据
UNDO是逻辑日志,只能将数据库逻辑上恢复到原来的样子。逻辑上取消了原来的修改,但是数据结构和页本身在回滚后可能大不相同。
- MVCC
多用户读取同一记录时,若记录已被修改,可以通过读取UNDO日志获取之前的版本信息,以此实现非锁定读取
2.3.存储结构
UNDO页
UNDO LOG采用段的方式管理,每个回滚段ROLLBACK SEGEMENT记录了1024个UNDO LOG SEGEMENT,在每个UNDO LOG SEGEMENT进行UNDO页的申请
- 在InnoDB1.1之前,只有一个ROLLBACK SEGEMENT,因此支持最多同时在线的事务限制为1024个。
- 从InnoDB1.1之后最大支持128个ROLLBACK SEGEMENT,支持同时在线的事务限制提高到了1024 * 128
- 从InnoDB1.2开始可以通过一些参数对ROLLBACK SEGEMENT做进一步的设置:
innodb_undo_directory:设置ROLLBACK SEGEMENT文件所在路径,默认为数据文件目录下
innodb_undo_logs:设置ROLLBACK SEGEMENT的个数,默认为128个innodb_undo_tablespaces:设置构成ROLLBACK SEGEMENT文件的数量,默认为0,表示不独立设置undo的tablespace,默认记录到系统表空间ibdata中
UNDO页的重用
当开启一个事务需要写UNDO LOG时,先去UNDO LOG SEGEMENT中申请UNDO页,1页的大小为16KB,如果一个事务分配一个页,会非常浪费空间,需要重用UNDO页:
在事务提交时,并不会马上删除UNDO页,会被放到一个链表中,判断使用空间是否大于3 / 4,如果小的话,就可以被重用,其他事务的内容可以记录在当前UNDO页后面。
由于UNDO页是离散的,清理对应磁盘空间时效率不高
回滚段与事务
- 每个事务只会使用一个回滚段,一个回滚段在同一时刻可能会服务多个事务
- 当一个事务开始时,会指定一个回滚段,数据被修改时,原始的数据会被复制到该回滚段
- 在回滚段中,事务会不断填充盘区,直到事务结束或所有空间耗尽。如果盘区耗尽,事务会在段中请求扩展下一个盘区,如果已分配的盘区都用完,事务会覆盖最初的盘区
- 回滚段处于UNDO表空间中,可以设置多个UNDO表空间,但同一时刻只能使用一个UNDO表空间
- 当事务提交时,存储引擎将UNDO LOG放入列表以便清除操作,并判断是否可以重用
回滚段数据分类
- 未提交的回滚数据:事务并未提交,用于实现读一致性,该数据不能被其它事务的数据覆盖
- 已提交未过期的数据:事务已提交,但仍在
UNDO RETENTION参数保持的时间 - 已提交过期的数据:事务已提交且保存时间超过了
UNDO RETENTION指定的时间,属于已经过期的数据,回滚段满后会优先覆盖这种数据
事务提交之后不能马上删除UNDO LOG,可能会有其他事务需要UNDO LOG来获取行记录之前的版本,故放到链表中,由purge线程判断删除与否
2.4.类型
insert undo log:由INSERT操作产生的,只对事务本身可见,对其他事务不可见(隔离性),可以直接删除不需要交给purge线程update undo log:由DELETE和UPDATE产生的,可能需要提供MVCC机制,不能在提交后就删除,需放入链表由purge判断
2.5.生命周期
- 生成过程
InnoDB中,每行记录的真实数据中有几个隐藏列:
DB_ROW_ID:行ID,唯一标识一条记录;如果表没有显示定义主键,并且也没有唯一索引,则自动以该隐藏列作为主键DB_TRX_ID:每个事务都有事务ID,当对某条记录发生改变时,就会将这个事务的事务ID写入该隐藏列中DB_ROLL_PTR:回滚指针,指向UNDO LOG的指针
执行以下例子:
BEGIN;
INSERT INTO user (NAME) VALUES ("tom");
此时插入的数据会生成一条INSERT UNDO LOG,并且数据的回滚指针会指向它,UNDO LOG会记录数据的主键值等,在进行ROLLBACK的时候,直接删除对应的数据即可
UPDATE user SET NAME = "Sun" WHERE id = 1;
更新的数据会产生UPDATE UNDO LOG,且区分是否更新主键,这时把原来的记录写入编号为1的UNDO LOG,回滚指针指向它再指向之前的UNDO LOG
UPDATE user SET id = 2 WHERE id = 1;
更新主键的时会把原来数据的DELETEMARK打开,还没有真正删除数据,交给清理线程判断;在后面插入一条新数据,新数据产生新的UNDO LOG,序号递增
可见每次对数据的变更都会产生序号递增的UNDO LOG,回滚的时候按照序号依次向前推就可以得到原始的数据