一、原子性
ACID:通过undolog保证原子性
二、undolog 如何做
把回滚时所需的东西都给记下来:
1、插入一条记录时,至少要把这条记录的主键值记下来,回滚的时候只需要把这个主键值对应的记录删掉就好了。
2、删除了一条记录,至少要把这条记录中的内容都记下来,回滚时再把由这些内容组成的记录插入 到表中就好了。
2、修改了一条记录,至少要把修改这条记录前的旧值都记录下来,回滚时再把这条记录更新为旧值 就好了。
三、事务id
3.1 分配事务id时机
某个事务执行过程中对某个表执行了增、删、改操作,那么 InnoDB 存储引擎就会给它分配一个独一无二的 事务id,如果只有读请求则不会分配事务id。
3.2 事务id如何生成
1、服务器会在内存中维护一个全局变量,每当需要为某个事务分配一个 事务id 时,就会把该变量的值当作 事 务id 分配给该事务,并且把该变量自增1。
2、每当这个变量的值为 256 的倍数时,就会将该变量的值刷新到系统表空间的页号为 5 的页面中一个称之为 Max Trx ID 的属性处,这个属性占用 8 个字节的存储空间。
3、当系统下一次重新启动时,将上边提到的 Max Trx ID 属性加载到内存,该值加上256之后赋值给我们 前边提到的全局变量(因为在上次关机时该全局变量的值可能大于 Max Trx ID 属性值)。
***保证整个系统中分配的事务id是一个递增的数字。先被分配 id 的事务得到的是较小的事务id , 后被分配 id 的事务得到的是较大的 事务id 。***
3.3 trx_id隐藏列
trx_id 列其实还蛮好理解的,就是某个对这个聚簇索引记录做改动的语句所在的事务对应的 事务id 而已 (此处的改动可以是 INSERT 、 DELETE 、 UPDATE 操作)。
四、undo日志的格式
4.1 insert对应的undolog
4.1.1 undolog日志结构
插入一条记录时有 乐观插入 和 悲观插入 区分,对应TRX_UNDO_INSERT_REC 类型的undolog
4.2.2 undolog记录实例
1、执行两个insert语句:
INSERT INTO undo_demo(id, key1, col) VALUES (1, 'AWM', '狙击枪'), (2, 'M416', '步枪');
2、第一条 undo日志 的 undo no 为 0 ,记录主键占用的存储空间长度为 4 ,真实值为 1 。画一个示意图就是 这样:
3、第二条undo日志 的 undo no 为 1 ,记录主键占用的存储空间长度为 4 ,真实值为 2 。画一个示意图就是 这样(与第一条 undo日志 对比, undo no 和主键各列信息有不同):
4.2 DELETE对应的undo log
4.2.1 删除操作数据结构
Page Header 部分有一个称之为 PAGE_FREE 的属性,被删除的记录其实也会根据记录头信息中的 next_record 属性组成一个链表,只不过这个链表中的记录占用的存储空间可以被重新利用,所以也称这个链表 为 垃圾链表。
4.2.2 删除操作2个阶段
1、阶段一:delete mark:仅仅将记录的 delete_mask 标识位设置为 1 ,其他的不做修改(其实会修改记录的 trx_id 、 roll_pointer 这些隐藏列的值),是一个 中间状态。
2、阶段二:purge 删除语句事务提交,有专门的线程后来真正的把记录删除掉。就 是把该记录从 正常记录链表 中移除,并且加入到 垃圾链表 中,还要调整一些页面的其他信息,比如页 面中的用户记录数量 PAGE_N_RECS 、上次插入记录的位置 PAGE_LAST_INSERT 、垃圾链表头节点的指针 PAGE_FREE 、页面中可重用的字节数量 PAGE_GARBAGE 、还有页目录的一些信息。
3、阶段二执行完成
将被删除记录加入到 垃圾链表 时,实际上加入到链表的头节点处,会跟着修改 PAGE_FREE 值。
4.2.3 删除操作日志结构
对应TRX_UNDO_DEL_MARK_REC类型的undolog,结构如下:
4.2.4 版本链
在对一条记录进行 delete mark 操作前,需要把该记录的旧的 trx_id 和 roll_pointer 隐藏列的值都给记 到对应的 undo日志 中来,对应图中的 old trx_id 和 old roll_pointer 属性。可以通过 undo日志 的 old roll_pointer 找到记录在修改之前对应的 undo 日志。比方说在一个事务 中,我们先插入了一条记录,然后又执行对该记录的删除操作,这个过程的示意图就是这样:
从图中可以看出来,执行完 delete mark 操作后,它对应的 undo log和 INSERT 操作对应的 undo 日志就串 成了一个链表。这个链表就称之为 版本链。
4.2.5 删除实例
BEGIN; # 显式开启一个事务,假设该事务的id为100
# 插入两条记录
INSERT INTO undo_demo(id, key1, col)
VALUES (1, 'AWM', '狙击枪'), (2, 'M416', '步枪');
# 删除一条记录
DELETE FROM undo_demo WHERE id = 1;
这个 delete mark 操作对应的 undo日志 的结构就是这样:
1、这条 undo 日志是 id 为 100 的事务中产生的第3条 undo 日志,所以它对应的 undo no 是 2 。
2、在对记录做 delete mark 操作时,记录的 trx_id 隐藏列的值是 100 (也就是说对该记录最近的一次修改就 发生在本事务中),所以把 100 填入 old trx_id 属性中。然后把记录的 roll_pointer 隐藏列的值取出 来,填入 old roll_pointer 属性中,这样就可以通过 old roll_pointer 属性值找到最近一次对该记录做改 动时产生的 undo日志 。
3、由于 undo_demo 表中有2个索引:一个是聚簇索引,一个是二级索引 idx_key1 。只要是包含在索引中的列,那么这个列在记录中的位置( pos ),占用存储空间大小( len )和实际值( value )就需要存储到 undo日志 中。
对于主键来说,只包含一个 id 列,存储到 undo日志 中的相关信息分别是:
pos:id 列是主键,在记录的第一个列,它对应的 pos 值为 0 。 pos 占用1个字节来存储。
len : id 列的类型为 INT ,占用4个字节,所以 len 的值为 4 。 len 占用1个字节来存储。
value :在被删除的记录中 id 列的值为 1 ,也就是 value 的值为 1 。 value占用4个字节来存储。 画一个图演示一下就是这样:
对于 id 列来说,最终存储的结果是 ,存储这些信息占用的存储空间大小为 1 + 1 + 4 = 6 个字节。
对于 idx_key1 来说,只包含一个 key1 列,存储到 undo日志 中的相关信息分别是:
pos : key1 列是排在 id 列、 trx_id 列、 roll_pointer 列之后的,它对应的 pos 值为 3 。 pos 占用1个字节来存储。
len : key1 列的类型为 VARCHAR(100) ,使用 utf8 字符集,被删除的记录实际存储的内容是 AWM ,所以一共占用3个字节,也就是所以 len 的值为 3 。 len 占用1个字节来存储。
value :在被删除的记录中 key1 列的值为 AWM ,也就是 value 的值为 AWM 。 value 占用3个字节 来存储。
key1 列来说,最终存储的结果是 ,存储这些信息占用的存储空间大小 为 1 + 1 + 3 = 5 个字节。
<0, 4, 1> 和 <3, 3, 'AWM'> 和 共占用 11 个字节。然后 index_col_info len 本身占用 2 个字节,所以加起来一共占用 13 个字节,把数字 13 就填到了 index_col_info len 的 属性中。
4.2.6 update操作对应undo log
4.2.6.1 不更新主键
1、 就地更新(占用空间不变)
每个列在更新前后占 用的存储空间一样大,直接在原记录基础上修改对应列的值。
2、 占用空间改变
先删掉旧记录,再插入新纪录;要先把这条旧的记录从聚簇索引页面中删除掉,然后再根据更新后列的值创建一条新的记录插入到页面中。删除 并不是 delete mark 操作,而是真正的删除掉,也就是把这条记录从正常记录链表 中移除并加入到 垃圾链表中。如果新创建的记录占用的存储空间大小不超过旧记录占用的空间,那么可以直接重用被加入到垃圾链表中的旧记录所占用的存储空间,否则需要在页面中新申请一段空间以供新记录使用,如果本页面内没有可用的空间的话,那就需要进行页面分裂操作,然后再插入新记录。
4.2.6.2 更新主键
1、将旧记录进行 delete mark 操作
2、根据更新后各列值创建新记录,将其插入到聚簇索引中(需重新定位插入的位置)。
4.3 roll pointer隐藏列的含义
占用 7 个字节的字段,本质上就是一个指向记录对应的 undo日志的指针 。
数据记录被存储到了类型为 FIL_PAGE_INDEX 的页面中(就是我们前边一直所说的 数据页 ), undo日志 被存放到了类型为 FIL_PAGE_UNDO_LOG 的页面中。
五、 undolog 页面结构
5.1 FIL_PAGE_UNDO_LOG页面 (undo页面)
5.2 Undo页面链表
5.2.1 单个事务中的Undo页面链表
一个事务多个语句,形成TRX_UNDO_PAGE_NODE链表:
链表的第一个节点:first undo page
其他页面:normal undo page
事务执行过程中,同一个 Undo页面 要么只存储 TRX_UNDO_INSERT 大类的 undo日志 ,要么只存储 TRX_UNDO_UPDATE 大类的 undo日志,事务执行过程中需要2个undo页面链表,一个是insert undo链表,一个是update undo链表:
记录表和临时表产生undo log,形成4个链表:
***按需分配,啥时候需要啥时候再分配,不需要就不分配***
5.2.2 多个事务undolog 链表
***不同事务执行过程中产生的undo日志需要被写入到不同的Undo页面链表中***
trx 1 对普通表做了 DELETE 操作,对临时表做了 INSERT 和 UPDATE 操作。
InnoDB 会为 trx 1 分配3个链表,分别是:
针对普通表的 update undo链表 。
针对临时表的 insert undo链表 。
针对临时表的 update undo链表 。
trx 2 对普通表做了 INSERT 、 UPDATE 和 DELETE 操作,没有对临时表做改动。
InnoDB 会为 trx 2 分配2个链表,分别是:
针对普通表的 insert undo链表 。
针对普通表的 update undo链表 。
5.3 小结
对于没有被重用的 Undo页面 链表来说,链表的第一个页面,也就是 first undo page 在真正写入 undo日志前,会填充 Undo Page Header 、 Undo Log Segment Header 、 Undo Log Header 这3个部分,之后才开始正式 写入 undo日志 。对于 normal undo page 页面在真正写入 undo日志 前,只会填充 Undo Page Header 。链表的 List Base Node 存放到 first undo page 的 Undo Log Segment Header 部分, List Node 信息存放到每一个 Undo页面 的 undo Page Header 部分,所以画一个 Undo页面 链表的示意图就是这样:
六、回滚段
设计新增页面类型为:Rollback Segment Header的页面,存储Undo页面链表的 frist undo page页号 ,他们把这些页号称之为 undo slot。