1.事务持久性
1.1 redo log格式
日志类型+表空间ID+页号+数据页偏移量+具体修改数据
log block 固定为512字节大小,redo log 文件也是一样按512字节来划分的,每个 redo log 文件的格式也是一样的,都由若干个512字节的块组成。
每个 redo log 文件由两部分组成:
前2048字节,也就是前4个block是用来存储一些管理信息。其中第1个 block 存储文件头信息,第2个和第4个存储checkpoint,第3个block保留未没用。
从第2048字节往后是用来存储 redo log block 的。
1.2 刷盘时机
主要有下面的一些时机会刷盘:
- log buffer 空间不足时
如果写入 log buffer 的日志占据了 log buffer 总容量的一半了,默认情况下也就是超过8MB的时候,此时就会把他们刷入到磁盘文件里去。
这种情况一般在高并发的场景下可能会出现,每秒执行了很多增删改SQL语句,产生的redo log 瞬间超过了8M,然后就立马触发刷新 log block 到磁盘。不过这种情况一般比较少。
- 事务提交时
一个事务提交的时候,必须把它的redo log都刷入到磁盘文件里去,只有这样,才能保证事务的持久性,才算事务提交成功了(这就是force log at commit机制,即在事务提交的时候,必须先将该事务的所有事务日志写入到磁盘上的日志文件中进行持久化)。如果在写入的过程中MySQL宕机了,那事务也就失败了。
比如前面的事务T2的 redo log 占据了3个block,在提交T2事务时,就必须把这3个block都刷入磁盘。
后台线程刷盘:后台有一个线程会每隔1秒,把redo log block刷到磁盘文件里去。
MySQL关闭的时候,redo log block都会刷入到磁盘里去。
做 checkpoint 的时候。这个后面会说。
需要注意的是,不管什么时机刷盘,redo log block 始终是顺序刷盘的,比如事务提交的时候,会把这个事务mtr之前的block都刷入磁盘。
比如下面的T1、T2事务,在事务T1提交的时候,虽然事务T2还没完成,但会把图中箭头所指的位置之前的block都刷入磁盘。这个刷盘是时时刻刻都在进行的,所以一次刷盘也不会有很多block。
1.3 刷盘策略
在提交事务的时候,InnoDB会根据配置的策略来将 redo log 刷盘,这个参数可以通过 innodb_flush_log_at_trx_commit 来配置。
可以配置如下几个值:
-
0:事务提交时不会立即向磁盘中同步 redo log,而是由后台线程来刷。这种策略可以提升数据库的性能,但事务的持久性无法保证。
-
1:事务提交时会将 redo log 刷到磁盘,这可以保证事务的持久性,这也是默认值。其实数据会先写到操作系统的缓冲区(os cache),这种策略会调用 fsync 强制将 os cache 中的数据刷到磁盘。
-
2:事务提交时会将 redo log 写到操作系统的缓冲区中,可能隔一小段时间后才会从系统缓冲区同步到磁盘文件。这种情况下,如果机器宕机了,而系统缓冲区中的数据还没同步到磁盘的话,就会丢失数据。
为了保证事务的持久性,一般使用默认值,将 innodb_flush_log_at_trx_commit 设置为1即可。
1.4 LSN
LSN就代表写入的日志总量,LSN 的初始值是 8704,占用8个字节,且是单调递增的。
每一组mtr都有一个唯一的LSN值与其对应,LSN 值越小,说明对应mtr中的redo log产生的越早
事务产生的mtr写入log block后,会将修改的脏页加入到Flush链表头部,Flush链表对应的描述信息块中会有两个属性来记录LSN信息:
oldest_modification
:记录mtr开始的LSN值。
newest_modification
:记录mtr结束时的LSN值。
接着另一个mtr写入后,可能Flush链表中已经存在了对应的脏页,此时会将mtr结束时的LSN值写入newest_modification,原本的oldest_modification则保持不变。
实际上Flush链表中的脏页就是按照修改发生的时间顺序进行排序,也就是按照oldest_modification代表的LSN值进行排序的。链表靠近尾部的是最早修改的,链表头部则是最新修改的。
SHOW ENGINE INNODB STATUS
; 命令查看当前InnoDB存储引擎中的各种LSN值的情况
---
LOG
---
Log sequence number 294669958009
Log flushed up to 294669958009
Pages flushed up to 294669957358
Last checkpoint at 294669957349
0 pending log flushes, 0 pending chkp writes
21957055 log i/o's done, 1.98 log i/o's/second
Log sequence number
:代表系统中的LSN值,也就是当前系统已经写入的redo log总量。
Log flushed up to
:代表当前系统已经写入磁盘的redo log量。
Pages flushed up to
:代表Flush链表尾部最早被修改的那个页面对应的oldest_modification属性值。
Last checkpoint at
:当前系统的checkpoint_lsn值。
1.5 checkpoint
redo log 只是为了系统崩溃后恢复脏页用的,如果对应的脏页已经刷新到了磁盘,那么就算崩溃后也用不着这部分 redo log 了,那么它占用的磁盘空间就可以被覆盖重用。如果脏页没有刷入磁盘,那么对应的 redo log 就必须保留着。
InnoDB 设计了一个全局变量 checkpoint_lsn
来代表当前系统中可以被覆盖的redo log总量是多少,这个变量初始值也是8704
。当脏页被刷入磁盘时,就会做一次 checkpoint 来计算 checkpoint_lsn 的值,并写入 redo log 文件中。
做 checkpoint 主要有两个步骤:
- 计算
checkpoint_lsn
脏页只要已经刷入磁盘,那他们对应的redo log就可以被覆盖,那如何判断哪些脏页已经刷入磁盘呢?
前面说过 Flush链表 中的脏页是按修改时间,也就是oldest_modification
代表的LSN值排序的,链表尾部的脏页就是最早修改的,它所对应的oldest_modification就是最小的一个LSN值,那这个LSN之前的脏页就是已经刷入磁盘的。
在做 checkpoint 时,其实就是将Flush链表尾部的脏页的oldest_modification
赋值给checkpoint_lsn
。
- 写入checkpoint
接着根据checkpoint_lsn计算对应的redo log文件日志偏移量checkpoint_offset。
InnoDB还设计了一个全局变量checkpoint_no,代表checkpoint的次数,每做一次checkpoint,这个值就会加1。
然后就会将这些信息写入日志文件组中的第一个日志文件的checkpoint中。至于存到 checkpoint1 还是 checkpoint2,则根据checkpoint_no来计算,如果是偶数,就写到checkpoint1,如果是奇数,就写入checkpoint2。
可以看到checkpoint中就有三个属性来存储这些信息:
checkpoint_no
写入 LOG_CHECKPOINT_NO
checkpoint_lsn
写入 LOG_CHECKPOINT_LSN
checkpoint_offset
写入 LOG_CHECKPOINT_OFFSET
1.6 恢复
恢复的起点
- 首先要读取日志组中的第一个 redo log 文件头部的两个 checkpoint,先比较其中的 checkpoint_no,哪个大就使用哪个 checkpoint。
- 然后读取 checkpoint_lsn,这个值之前的都是已经刷盘了的,但之后的可能刷盘了,也可能没有刷盘。所以恢复的起点就是 checkpoint_lsn 对应的文件偏移量,从这个偏移量开始读取 redo log 来恢复页面。
- 恢复的终点
redo log block 的头部header中有一个属性LOG_BLOCK_HDR_DATA_LEN
记录了当前block里使用了多少字节的空间,对于被写满的block来说,该属性就是512。如果该属性的值不为512,说明这个block还没写满,那终点就是这个block了。 - 使用哈希表
读取到内存中的 redo log,并不是直接就按顺序去重做页的。而是使用了一个哈希表来加快恢复的速度。它会根据 redo log 的表空间ID和页号计算出散列值,以此作为哈希表的 Key,哈希表的 Value 则是一个链表,相同表空间ID和页号的 redo log 就会挨个按顺序加入这个链表中。
之后就遍历哈希表来恢复页,因为对同一个页面修改的 redo log 都在一个链表中,所以可以一次性将一个页面修复好(避免了很多读取页面的随机IO),这样可以加快恢复速度。 - 跳过已经刷新到磁盘的页面
checkpoint_lsn
之前的可以保证 redo log 对应的脏页已经刷盘了,但是之后的就不能确定了。因为在做 checkpoint 之后,可能一些脏页会不断的被刷到磁盘中,那这部分 redo log 就不能在页中重做一遍。
这个时候就会用到前面说过的页中的FIL_PAGE_LSN
属性,这个属性记录了最近一次修改页面对应的LSN值。如果在做了某次checkpoint之后有脏页被刷新到磁盘中,那么该页对应的FIL_PAGE_LSN代表的LSN值肯定大于checkpoint_lsn的值,对于这种页面就不需要在应用 redo log 了。
2.事务原子性
通过Undo log保证事务原子性
Undo Log
会产生Redo Log
,也就是Undo Log的产生会伴随着Redo Log的产生,这是因为Undo Log也需要持久性的保护
2.1 undo log功能
- 用于崩溃后的回滚恢复
- 用于实现innodb中的mvcc 当用户读取一行记录时,若该记录已经被其他事务占用,当前事务可以通过Undo Log读取之前的行版本信息,以此实现非锁定读取。
2.2 undo log存储结构
- insert undo
假设这个事务的事务ID为100,这条INSERT语句会插入两条数据,就会产生两个undo log
。插入记录的时候,会在行记录的隐藏列事务ID中写入当前事务ID,并产生 undo log,记录中的回滚指针会保存 undo log 的地址。而同一个页中的多条记录会通过next_record
连接起来形成一个单链表
2.3 delete undo
首先标记 delete_mask
为1,而不是直接从页中删除,因为可能其它并发的事务还需要读取这条数据
事务提交后,后台线程开始purge
页中的数据是通过记录头信息中的 netx_record 连接起来的单向链表(假设这个链表称为数据链表)。页中还有另一个链表,称为垃圾链表,记录真正删除后,会从数据链表中移除,然后加入到垃圾链表的头部,以便重用空间
start、end
:指向记录开始和结束的位置。
undo type
:undo log 的类型,也就是 TRX_UNDO_INSERT_REC。
undo no
:在当前事务中 undo log 的编号。
table id
:表空间ID。
主键列信息
:这一块就需要记录INSERT这行数据的主键ID信息,或者唯一列信息
old trx_id
:这个属性会保存记录中的隐藏列trx_id,这个属性在MVCC并发读的时候就会起作用了。
old roll_pointer
:这个属性保存记录中的隐藏列roll_pointer,这样就可以通过这个属性找到之前的 undo log。
索引列信息
:这部分主要是在第二阶段事务提交后用来真正删除记录的
BEGIN;
INSERT INTO account(id,card,balance) VALUES (1, 'AA', 0),(2, 'BB', 0);
DELETE FROM account WHERE id = 2;
2.4 update undo
2.4.1 不更新主键
- 存储空间未发生变化
更新记录时,对于被更新的每个列来说,如果更新后的列和更新前的列占用的字节数都一样大,那么就可以进行就地更新
,也就是直接在原记录的基础上修改对应列的值。
- 存储空间发生变化
如果有任何一个被更新的列更新前和更新后占用的字节数大小不一致,那么就会先把这条旧的记录从聚簇索引页面中删除掉,然后再根据更新后列的值创建一条新的记录插入到页面中。注意这里的删除并不是将 delete_mask
标记为 1,而是真正的删除
,从数据链表中移除加入到垃圾链表的头部。
如果新的记录占用的存储空间大小不超过旧记录占用的空间,就可以直接重用刚加入垃圾链表头部的那条旧记录所占用的空间,否就会在页面中新申请一段空间来使用。
BEGIN;
INSERT INTO account(id,card,balance) VALUES (1, 'AA', 0),(2, 'BB', 0);
DELETE FROM account WHERE id = 2;
UPDATE account SET card = 'CC' WHERE id = 1;
2.4.2 更新主键
-
首先将原记录做标记删除,就是将
delete_mask
改为 1,还没有真正删除。 -
然后再根据更新后各列的值创建一条新记录,并将其插入到聚簇索引中
-
第一步标记删除时会创建一条
TRX_UNDO_DEL_MARK_REC
类型的 undo log。 -
第二步插入记录时会创建一条
TRX_UNDO_INSERT_REC
类型的 undo log。
更新主键后,原本的记录就被标记删除了,然后新增了一个 TRX_UNDO_DEL_MARK_REC
的 undo log。接着插入了一条新的id=3的记录,并创建了一个新的 TRX_UNDO_INSERT_REC
类型的 undo log
在回滚的时候,就可以应用这个事务中的 undo log,根据 undo no 从大到小开始进行撤销操作
2.5 undo 分类
undo log 分为两大类,不能混着存储,所以如果事务中产生了这两大类型的 undo log,会创建两个链表,一个用来存储 TRX_UNDO_INSERT
类别的 undo log,
一个用来存储 TRX_UNDO_UPDATE
类别的 undo log。
如果事务中还修改了临时表,InnoDB规定对普通表和临时表修改产生的 undo log 要分开存储,所以在一个事务中最多可能会有4个 undo 页面链表。
2.6 恢复undo
在图中箭头处,如果T1事务执行完成提交事务,此时 redo log 就会刷盘。而T2
事务还未执行完成,但它的 mtr_T2_1
已经刷入磁盘了。如果此时数据库宕机了,T2
事务实际是执行失败的。在重启数据库后,就会读取 mtr_T2_1
来恢复数据,而T2
事务实际是未完成的,所以这里恢复数据就会导致数据有问题。
redo log 恢复时,同样会对 undo 页重做,mtr_T2_1
这段 redo log 对数据页重做后,由于T2事务未提交,就会用 undo log 来撤销这些操作。就解决了这个问题。
2.7 重用undolog
undo 链表可以被重用的条件:
- 在 undo 页面链表中只包含一个 undo 页面时,该链表才可以被下一个事务所重用。因为如果一个事务产生了很多 undo log,这个链表就可能有多个页面,而新事务可能只使用这个链表很少的一部分空间,这样就会造成浪费。
- 然后该 undo 页面已经使用的空间小于整个页面空间的 3/4时才可以被重用。
对于TRX_UNDO_INSERT
类型的 insert undo 页面链表,这些 undo log 在事务提交之后就没用了,可以被清除掉。所以在某个事务提交后,重用这个链表时,可以直接覆盖掉之前的 undo log。
对于TRX_UNDO_UPDATE
类型的 update undo 页面链表,这些 undo log 在事务提交后,不能立即删除掉,因为要用于MVCC。所以重用这个链表时,只能在后面追加 undo log,也就是一个页中可能写入多组 undo log。