从ibd文件聊聊undo log

1,183 阅读8分钟

一、序

  • 0 以下基于mysql 5.7.28,InnoDB存储引擎,行格式等均为默认值。
  • 1 通过ibd文件来直观的了解innoDB是如何存储(组织)一行数据的
  • 2 各种技术文章说的每一行的ROWID、TransactionID、Roll Pointer到底是什么
  • 3 对innodb的段区页行有一定了解,看过姜承尧的InnoDB存储引擎的欢迎探讨学习
  • 4 非鸡汤文、标题党、拼凑公众号文章等,没有精美的图文,纯粹自己这2天的探究记录

二、测试表及测试数据如下

CREATE TABLE `T1` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(20) DEFAULT NULL,
  `age` varchar(8) DEFAULT NULL,
  `nick` varchar(5) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4;

2.1、先看一下表空间概览

2.2、表空间中得出的结论

1.从图可以看出,indexId =171 类型是PRIMARY的索引,有2个段(internal和leaf)。
  事实上每一个索引都有这2个段,通过叫做叶子段、内部节点段(非叶子段)。也有叫索引段、数据段。
2.由于表的数据量很少,索引段、数据段都只有一层。
3.page offset=3的页容纳了所有的数据。
4.为了后文演示null字段,我在表创建填充部分数据之后,增加了age列,nick列。后来发现这个第4页看不明白,仔细研究后发现原来这个是free页。如有读者遇到知道这个即可。

三、t2.ibd文件

  • 为了让读者不建库表也可以了解一下ibd文件中某一行的存储知识,将上表的ibd文件hexdump出来,因为数据主要在page offset=3的页,故只截取分析此部分。

  • innoDB中一页通常16k,从0xc000开始为page =3的页。

  • 先看下这页的数据(record 偏移量 数据)。

  • 可以看出此页中有6条数据,每一行存储了行中所有的列数据。
  • 还可以看出一页中并不是主键大的一定依次排在主键小的后面,如本页中主键id=6的offset是262,主键id=1的offset是290。

3.1、敲黑板---ibd文件中定位某一行

3.1.1、伪记录行

  • 关于innoDB的2个伪记录行可以参考姜大佬的书。这里分析一下上图中id=1的offset=290是怎么得来的?
    • 0xc05E开始:01 00 02 00 bf 69 6e 66 69 6d 75 6d 00。即按照规则 0x0063+0xbf=0x0122(十进制的290)。
  • 让我们跳到0xc000+0x122=0xc122处

3.1.2、狠敲黑板---手把手解读一行数据

  • 系统字段

    • 00 00 00 01 :主键id=1(unsigned int,有符号的话会是80 00 00 01),int占用4字节。
    • 00 00 00 00 28 b1:紧跟在主键之后的6字是TransactionID,换成十进制是10417。
    • 35 00 00 01 40 21 1a:紧跟着的7字节Roll Pointer回滚指针。
  • 此处说明如下四点:

    • 1、很多文章提到每一行中会有三个隐藏列RowID、TransactionID、Roll Pointer,部分文章说是两列。我在刚开始学习的时候也很困惑。其实这些文章再多交代一点或许我就不会有这个疑惑。这里我可以说的很清楚:在明确指定主键的情况下,就不会生成RowID(或者说主键的值就是RowID的值)。如果没有指定则innoDB会负责生成一个6字节值做主键。
    • 2、指定主键下TransactionID、Roll Pointer才可以称为系统字段(System field)。
    • 3、关于事务ID可能比较容易理解,一个个事务有先后,事务id的先后分大小。innoDB的可见性以及MVCC中都会跟事务ID有联系。RC、RR隔离级别下的read view都会用到。
    • 4、我一直最困惑的是回滚指针。指针嘛,一串数字,它指的地方存的是什么?我一直没有一个直观的理解。直到这两天细细追究这个问题。且看下图

3.1.3、ROLL POINTER解读

  • 事务id = 10417与我们分析的一致。
  • Roll Pointer中我们看到了undo log、Rollback Segment,我看到这个立马直观了起来,也能理解了:回滚指针指向了undo log,undo log是一个个指针串起来的保留数据修改前的记录链条。
  • 其实7字节的回滚段并不是一串冰冷的数字,它包含三大部分信息。
    • 35:第一个字节指向的是回滚段(也终于明白了回滚段大小最多255,因为只有1个字节保存)。
    • 00 00 01 40:4字节,指向的page no,0x140=320
    • 40 21:2字节的偏移量 0x4021=8474(1页16k,2字节能保存到所有的偏移量)
    • 至于insert false没有研究。

3.1.4、数据字段中变成字段及NULL标志位解读

  • 系统字段完了,紧跟着的是数据字段。

  • 看一下主键之前的字段(图中红框标注出来的是主键)。下一行结尾到本行主键之前的值 03 03 04 00 00 40 00 1f怎么解读?

  • 在贴出一张第一行的数据图。

  • 按照姜大佬的方式,这部分是变长字段列表和NULL标志位相关。具体分析如下

    • 1、name、age、nick均是varchar都是变长字段,其中该行nick是NULL。NULL不占用实际存储空间。03 03分别"hel","aaa"的长度。此时没毛病。
    • 2、那04就是NULL标志位了,换成01就是100。即第3列是NULL,不科学啊,我这第3列是age明明是"hel"。看姜大佬的06NULL标志位是0110解释:第2列、第3列是NULL。我这解释的怎么都对不上列,为此建了好几张表,好几个列。总是差一列。
    • 3、后来我才悟出这计算应该是除主键外的普通字段开始是第一列,即此处是name第一列,age第二列。。。姜大佬的建表t1、t2、t3、t4四个字段,他没有主键。所以系统会生成ROWID。他这个t1、t2、t3、t4就是真实的第1,2,3,4列,所以不会出现错一列的现象。

四、最后一提undo log

  • 看id=1这行ROLL POINTER指向page=320,offset=8474,我们来看下这个page的offset处值是什么?

4、1一个undo log链的分析

  • 这是320页offset=8474的一张图,可以看出这是主键id=1的一行,非主键的值age什么的不太直观,这是我建表时候的NULL还是新增的列为NULL时候的数据也忘记。下面改一条数据看看,将id=2的name='ccc',nick='hhee'的改成'22222','33333'看看。

  • 先记录原始id=2的数据

  • 改完之后先来打印一下page=3的

  • 可以看到原来id=2的offset发生了变化,与传统的理解在offset的原位置改几个值不一样。其实也能理解,背后也体现了MVCC。因为某些事务可能还引用着这个offset的值。所以update其实也可以理解为一行新记录的生成(新undo log)

4.2 update之后

  • 打印一下undo log即page=381 ,offset=8251

  • 可以看到这个undo log记录着更新前的值。无论是回滚还是MVCC的读取都可以找到正确的值。

五、关于MVCC中的非锁定快照读

  • 大家都听过,但是理解多少因人而异。在前天以前,我对MVCC的理解只停留在这么一句:快照读,不加锁,提高并发。但是怎么实现的?很多文章提到undo log,但是也止步于此。(淘宝的数据库月刊有不少分析的,挺复杂,看不下去)。
  • 通过以上分析,大家应该有些直观。每一行数据中隐藏的系统字段就是实现MVCC的关键。其中ROLL POINTER明确的指向一个undo log(某个页+某个偏移量)。一个undo log又指向上一个undo log,依次串起来。不同的事务通过事务id来看到这一行他该看到的字段值,可能不是最新的。不需加锁,支持高并发。这也是快照读的由来,因为这行数据此时就是有多个版本的快照。
  • MVCC博大精深,既没精力也没实力说清楚。以后搞懂了在说。

六、总结

  • 1 通过对一行数据的解读来了解InnoDB是怎么组织一行数据的。
  • 2 ROLL POINTER并不是一串简单的数字,背后是undo log的身影,也是实现MVCC的基础。