ACID:通过undolog保证原子性
把回滚时所需的东西都给记下来:
1、插入一条记录时,至少要把这条记录的主键值记下来,回滚的时候只需要把这个主键值对应的记录删掉就好了。
2、删除了一条记录,至少要把这条记录中的内容都记下来,回滚时再把由这些内容组成的记录插入 到表中就好了。
2、修改了一条记录,至少要把修改这条记录前的旧值都记录下来,回滚时再把这条记录更新为旧值 就好了。
某个事务执行过程中对某个表执行了增、删、改操作,那么 InnoDB 存储引擎就会给它分配一个独一无二的 事务id,如果只有读请求则不会分配事务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 。***
trx_id 列其实还蛮好理解的,就是某个对这个聚簇索引记录做改动的语句所在的事务对应的 事务id 而已 (此处的改动可以是 INSERT 、 DELETE 、 UPDATE 操作)。
插入一条记录时有 乐观插入 和 悲观插入 区分,对应TRX_UNDO_INSERT_REC 类型的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 和主键各列信息有不同):
Page Header 部分有一个称之为 PAGE_FREE 的属性,被删除的记录其实也会根据记录头信息中的 next_record 属性组成一个链表,只不过这个链表中的记录占用的存储空间可以被重新利用,所以也称这个链表 为 垃圾链表。
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 值。
对应TRX_UNDO_DEL_MARK_REC类型的undolog,结构如下:
在对一条记录进行 delete mark 操作前,需要把该记录的旧的 trx_id 和 roll_pointer 隐藏列的值都给记 到对应的 undo日志 中来,对应图中的 old trx_id 和 old roll_pointer 属性。可以通过 undo日志 的 old roll_pointer 找到记录在修改之前对应的 undo 日志。比方说在一个事务 中,我们先插入了一条记录,然后又执行对该记录的删除操作,这个过程的示意图就是这样:
从图中可以看出来,执行完 delete mark 操作后,它对应的 undo log和 INSERT 操作对应的 undo 日志就串 成了一个链表。这个链表就称之为 版本链。
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 的 属性中。
占用 7 个字节的字段,本质上就是一个指向记录对应的 undo日志的指针 。
数据记录被存储到了类型为 FIL_PAGE_INDEX 的页面中(就是我们前边一直所说的 数据页 ), undo日志 被存放到了类型为 FIL_PAGE_UNDO_LOG 的页面中。