说明
文章的图片来源《MySQL是怎么运行的:从根儿上理解MySQL》,本篇文章只是个人学习总结,欢迎大家买一本正版小册看看,对于mysql是由浅入深的讲解非常细致
21.redo日志(下)
redo日志文件
redo日志刷盘时机
- log buffer内存不够的时候
- 事务提交的时候
- 后台线程不停刷,每秒会刷新一次
- 关闭服务器的时候
redo日志文件组
- innodb_log_group_home_dir:redo日志所在的目录
- innodb_log_file_size:每个redo日志大小,默认是48MB
- innodb_log_files_in_group:日志文件组通常是2个(ib_logfile1,2…)
- 如果写完一个文件怎么办?就会重新从第一个文件开始写
redo日志文件格式
两部分组成
- 前2048个字节也就是4个block用来管理信息
- 往后的字节都是用来存储log buffer上面的block镜像(这个其实就是log buffer上的block)的
- redo日志的前四个block
- 对于log file header的属性
- LOG_HEADER_FORMAT:redo日志版本,通常是1
- LOG_HEADER_PAD1:字节填充
- LOG_HEADER_START_LSN :标记日志文件开始的LSN值
- LOG_HEADER_CREATOR:文件的创建者,通常是mysql的版本号
- LOG_BLOCK_CHECKSUM :校验block值
- checkpoint1
- LOG_CHECKPOINT_NO :编号,每做一次checkpoint就加1
- LOG_CHECKPOINT_LSN:做checkpoint结束的LSN值,如果系统崩溃就从这里开始
- LOG_CHECKPOINT_OFFSET:LSN值在文件组偏移量
- LOG_CHECKPOINT_LOG_BUF_SIZE:在做checkpoint的log buffer的大小
- LOG_BLOCK_CHECKSUM:本block的校验值
Log Sequeue Number
- 日志序列号就是LSN,初始是8704
- 第一次初始化log buffer,这个时候12个字节是管理信息头,buf_free会指向第一个可以写的地方,也就是12字节偏移处,并且LSN也会+12
- 如果mtr产生的redo日志比较小,LSN增加的数量就是mtr产生日志的字节数
- 如果mtr产生的日志比较大的时候,除了日志占用空间还需要加上log block header和log block tailer的占用的字节数
- 每组mtr都有对应的LSN,LSN越小,说明日志产生的时间就越早
flushed_to_disk_lsn
- buf_next_to_write:记录的是刷新到的位置,也就是它前面的日志都已经刷新到磁盘了
- flushed_to_disk_lsn:lsn是系统写入到redo log上面的日志序列号,但是flushed_to_disk_lsn就是刷新到redo log上面日志序列号,和lsn并不会同步
- 下面是演示,系统写入3个mtr。然后没有刷新到磁盘的时候。lsn和flushed_to_disk_lsn拉开了差距
lsn值和redo日志文件偏移量的对应关系
- 计算LSN的文件偏移位置是非常简单的。
flush链表中的LSN
- mtr修改完页面之后还需要把对应的脏页写到flush链表头部上
- 脏页节点在flush链表上是按照时间从大到小排列的,两个关于页面何时修改的属性
- oldest_modification:第一次被修改的时候,mtr开始时的lsn写到这里
- newest_modification:每一次修改都把mtr结束的lsn写入到这里
- mtr1修改了a页面,开始的时候lsn是8716,结束的时候是8916
- mtr2同时修改b和c,开始的lsn是8916,结束的时候9948。
- 后面mtr3修改了d和b,b由于之前修改过,只需要修改lsn最后的位置就可以了,并不需要重新插入链表的头部。
总结
对于mtr的每次修改,第一次修改插入到flush头部并且记录开始的lsn和结束修改的lsn。如果是多次更新就不会重复加入到flush链表头部而是直接更新最后一次的lsn结束位置
checkpoint
明确redo日志的目的:为了系统崩溃的时候恢复脏页(内存修改的页面,但是没有更新到磁盘),如果已经更新到了磁盘,那么redo日志就已经失去了作用。这些空间又可以被其它的更新操作使用。
- 这个时候4个脏页面还没有刷新到磁盘,所以不能够被覆盖使用
- 下面是a页面刷新到了磁盘,所以磁盘里面的mtr1相关的日志已经没有作用了。可以被覆盖使用
-
checkpoint_lsn:表示当前系统可以被覆盖的redo日志总量,初始还是8704
-
页面a如果被刷新了,那么checkpoint_lsn就可以增加,这个过程就是checkpoint,两个步骤
- 计算当前系统被覆盖的redo日志对应的lsn最大是多少
- 计算出最早被修改的脏页的oldest_modification,lsn小于它的都可以被覆盖。把值赋予给checkpoint_lsn,比如现在的a刷新了,那么就只剩下c了,就把它的oldest_modification作为checkpoint_lsn的新值
- 把checkpoint_lsn、checkpoint_no和checkpoint在文件的偏移量写到redo日志文件组的管理信息中,通常checkpoint有1和2,奇数放到1,偶数放到2
- 计算当前系统被覆盖的redo日志对应的lsn最大是多少
-
相当于就是flushed_to_dist_lsn管理的是刷新到磁盘的log,lsn是最新写入的log,checkpoint是脏页写到磁盘已经可以被覆盖的log。
批量从flush链表中刷出脏页
- 如果修改数据太多,导致写入的日志非常多,快速占满了log buffer,这个时候就需要用户线程去把flush链表最早的脏页批量刷新到磁盘,让checkpoint及时让出位置来加入新的log。
查看系统中的各种LSN值
- Log sequence number:系统的lsn值,已经写入系统的日志量
- Log flushed up to:flushed_to_disk_lsn写入磁盘的日志量
- Pages flushed up to:flush链表最早修改的那个oldest_modification属性值
- Last checkpoint at:当前系统的checkpoint_lsn值
innodb_flush_log_at_trx_commit的用法
如果每次事务提交都需要把redo日志提交到磁盘上面,就会耗费很大的性能,innodb_flush_log_at_trx_commit系统变量可以改变策略
- 0:提交不会立刻刷新到磁盘,而是等待后台线程来进行刷新
- 1:提交的时候就要把日志同步到磁盘
- 2:事务提交,日志写到redo日志的缓冲区,不需要一定要写入到磁盘,因为申请的内存在操作系统,所以mysql挂了没事,只要操作系统没挂
崩溃恢复
恢复过程
确定恢复起点
- 从checkpoint_lsn开始,这个lsn之前都是已经可以被覆盖的,后面的就是要么还没有刷新到磁盘,要么就是已经刷新到磁盘的log
- 两个block保存了checkpoint的信息,选取最近发生的checkpoint,早晚可以通过checkpoint_no来进行确定。并且求出checkpoint的offset
确定恢复的终点
- 普通的log block header有LOG_BLOCK_HDR_DATA_LEN记录在该block使用了多少个字节,只需要扫描最后一个使用的字节不是512的block那么就是最新更新的block的结尾,获取这里的lsn。
怎么恢复
- 扫描checkpoint到最新lsn的位置,并且调用函数进行恢复操作。但是可以加快
- 哈希表:表空间id和page number相同的放到同一个位置,链表串起来,每次都可以一下子刷新掉这个链表的页的所有修改,并且按照时间顺序进行恢复
- 跳过已经刷新到磁盘的页面
因为在chepoint的时候,由于是多线程操作,checkpoint同时也可能有新的脏页刷新到磁盘,问题是如何知道脏页已经刷新到磁盘?每个页面的File Header的FIL_PAGE_LSN记录最近一次的修改lsn,如果被刷新了lsn肯定大于checkpoint_lsn。这种页面就不需要根据redo日志进行恢复
LOG_BLOCK_HDR_NO是如何计算的
- log block header上面有LOG_BLOCK_HDR_NO代表一个唯一的标号((lsn / 512) & 0x3FFFFFFFUL) + 1
- ((lsn / 512) & 0x3FFFFFFFUL) + 1这个值在00x3FFFFFFFUL之间,加1之后就是10x40000000UL,最多只能产生1GB的不重复的block块
总结
- redo日志文件的一个刷新时机
- 提交事务
- 后台线程
- redo日志文件通过文件组的方式进行存储,可以有多个这样的问题
- redo日志文件的格式,通过block为单位进行存储,都有header和tailer
- LSN是redo日志文件的关键,包括控制chekpoint位置(也就是日志对应的脏页已经刷新到磁盘不需要再使用),另外还有flushed_to_disk_lsn(刷新到磁盘的redo日志),还有就是仅仅只是刷新到缓存区的lsn。这里的缓存区是在操作系统上面的,所以服务器崩溃并不会影响存储在缓存区的redo日志。
- 最后就是崩溃恢复的步骤
- 起点是在checkpoint位置
- 终点就是block没有满512字节的位置
- 最后就是可以通过给指定的内容和参数给函数,来进行恢复。
22.undo日志(上)
事务回滚的需求
- 情景1:事务执行到一半,出现错误
- 情景2:手动回滚
- 所以需要让事务看起来什么都没做,就需要进行回滚
回滚之前的操作
- 插入需要记录主键值,才能回滚删除
- 删除需要保存记录的内容,才能回滚插入
- 修改需要记录旧值
记录这些东西的就是undo log,回滚日志
事务id
给事务分配id的时机
- START TRANSACTION READ ONLY开启一个只读事务,对临时表进行增删改
- START TRANSACTION READ WRITE开启一个读写事务
如果事务对表进行了增删改就需要加上一个事务id
- 对于只读事务来说,用户创建临时表在增删改才会为事务分配事务id
- 读写事务:对表第一次增删改才会为事务分配id
事务id是怎么生成的
- 维护一个全局变量,变量自增并且赋值
- 如果变量到达256的倍数的时候,去到5页面更新Max Trx ID
- 系统下次启动就会把Max Trx ID加载进内存,并且加上256赋值给全局变量,因为全局变量可能比上一次的Max Trx ID更大,如果不这样赋值可能会有重复的事务id。
trx_id隐藏列
- 这里的id的意思就是记录做改动所在的事务的事务id
undo日志的格式
- undo_no就是每条日志的编号
- FIL_PAGE_UNDO_LOG这个就是undo log的页面类型从系统表空间上面去分配
INSERT操作对应的undo日志
接下来就是使用这个表
CREATE TABLE undo_demo
( id INT NOT NULL,
key1 VARCHAR(100),
col VARCHAR(100),
PRIMARY KEY (id),
KEY idx_key1 (key1) )Engine=InnoDB CHARSET=utf8;
TRX_UNDO_INSERT_REC
- undo_no:每生成一条undo日志,这个日志的no就会加1
- 如果主键只有一个列,那么就直接存储这个列的值和存储空间,因为删除操作会把聚簇和二级索引的值同时删除。
现在插入两条数据到undo_demo
- 然后就会有两条undo_log数据,分别记录了,表的id,主键的存储大小和值。
roll_pointer
实际上就是指向记录对应的undo日志。日志被存放到FIL_PAGE_UNDO_LOG类的页面上。
- 每个记录都会有自己的一条undo日志,也就是会对应着一个roll_pointer指针。
DELETE操作对应的undo日志
- 正常记录有自己的一个记录链表,同样被删除的节点也有自己的一个链表
- 并且被Page Header的Page Free指向已经删除记录的头结点
现在删除最后一条记录过程
- 阶段1:把delete_mask标识为1,这个阶段被称为delete mark,这种是中间状态
删除事务还没有提交那么就会处于这种中间状态,没有转移到垃圾链表
- 阶段2:事务提交之后会有线程专门进行处理,删除记录放到垃圾链表,还要修改PAGE_N_RECS、PAGE_LAST_INSERT上次插入的位置、PAGE_FREE垃圾链表头结点、PAGE_GARBAGE页面可以使用的字节,这个过程是purge
- 被删除的节点实际上是会被放到垃圾链表的头结点
- PAGE_GARBAGE的作用其实就是可以用于处理空间碎片问题
- 如果新插入的记录空间大于这个PAGE FREE指向的垃圾链表头节点,那么就重新向页面请求空间。
- 如果是小于等于就可以直接重用垃圾链表头结点。并且把节点迁移到正常记录的链表
- 如果实在没有空间,但是PAGE_GARBAGE有足够空间,那么就重新组织所有的链表,并且整理出空间给新的记录
TRX_UNDO_DEL_MARK_REC
- 在delete mark之前需要把trx_id和roll_pointer的值记录到undo日志,现在是先插入一条记录,这个时候记录的roll_pointer肯定就是指向insert这条undo日志,也就是记录了insert的这个主键和值方便回滚删除这条记录。那么现在又执行了一个删除操作,那么就需要新记录的roll_pointer指向delete undo这条新的undo日志。并且在这之前还需要保存之前的insert undo日志。并且让delete undo的roll_pointer指向insert_undo方便之后的回滚。也就是日志之间做成了一条链表,这条链表就是版本链
- TRX_UNDO_DEL_MARK_REC还增加了各个索引列的信息,实际上就是在索引上的索引列,需要保存值,索引的pos位置,还需要保存len长度。
下面就是插入之后再删除的一个版本链
-
删除undo日志是第三条日志,因为之前插入了两条记录。所以现在它是trx_id=100事务的产生的第三条日志
-
记录的最近一次事务是id=100(最近一次修改本记录的事务就是本删除事务),所以存入old trx_id中,而且隐藏列roll_pointer是指向insert这条回滚日志,所以也给删除日志的old roll_pointer进行赋值,这样就可以找到最近的一次修改产生undo log
- 只要是被包含在索引的索引列都需要存入pos(索引第几个列)、len(列占用的字节数)、value(值)
- 下面是id是int,所以value占用四个字节
- 对于idx_key1来说就是
- pos:在id、trx_id、roll_pointer列之后,pos的值是3
- len:varchar(100),使用utf-8,最后awm占用了3个字节
- value:value要使用3个字节进行存储
- 最后就是把所有索引列占用的空间字节写到index_col_info len属性
UPDATE操作对应的undo日志
不更新主键的情况
分为更新的列存储空间是否发生变化。
- 就地更新
这种更新的前提是更新之后的占用字节数和当前的占用字节数相同,也就是存储空间在更新前后没有发生变化,那么就可以在当前节点直接进行更新
- 删除旧的,插入新的
这里的删除是真正删除,直接删除掉聚簇索引页面上的记录,修改PAGE_FREE和PAGE_GARBAGE。如果新记录能重用垃圾链表的节点,那么就使用,如果不能,那么就只能重新申请了。
TRX_UNDO_UPD_EXIST_REC
- n_updated表示有多少个列被更新了,<pos, old_len, old_value>
- 如果更新的列包含索引列,那么也是需要把索引列各项信息填上去的。
下面这个是插入之后删除然后再更新
BEGIN; # 显式开启一个事务,假设该事务的id为100
# 插入两条记录
INSERT INTO undo_demo(id, key1, col)
VALUES (1, 'AWM', '狙击枪'), (2, 'M416', '步枪');
# 删除一条记录
DELETE FROM undo_demo WHERE id = 1;
# 更新一条记录
UPDATE undo_demo
SET key1 = 'M249', col = '机枪'
WHERE id = 2;
- 这个是id=100事务做的一个处理,而且是第4个产生的日志
- roll_pointer指向那条undo_no是1的日志。最近一次的修改。
- 更新了key1,那么就要写入到更新列和索引列上。并且索引列还需要记录主键的列。
更新主键的情况
-
旧记录做一个delete mark,提交后purge,删除会记录一条TRX_UNDO_DEL_MARK_REC的undo日志
-
根据更新信息创建新的一条记录插入,接着就是做一条TRX_UNDO_INSERT_REC的undo日志。
-
也就是主键改动需要多记录两条日志
DATE undo_demo
SET key1 = ‘M249’, col = ‘机枪’
WHERE id = 2;
[外链图片转存中...(img-jB33sb0k-1636338466195)]
[外链图片转存中...(img-txVB8B9l-1636338466196)]
- 这个是id=100事务做的一个处理,而且是第4个产生的日志
- roll_pointer指向那条undo_no是1的日志。最近一次的修改。
- 更新了key1,那么就要写入到更新列和索引列上。并且索引列还需要记录主键的列。
### 更新主键的情况
- 旧记录做一个delete mark,提交后purge,删除会记录一条TRX_UNDO_DEL_MARK_REC的undo日志
- 根据更新信息创建新的一条记录插入,接着就是做一条TRX_UNDO_INSERT_REC的undo日志。
- 也就是主键改动需要多记录两条日志