本文章翻译自 https://blog.jcole.us/2013/05/02/how-does-innodb-behave-without-a-primary-key/
原文作者的创作背景
一个下午,好基友(Arjen Lentz)和“我”讨论InnoDB在没有声明主键时候的是如何运作的,这个话题足够有趣并且又没有足够多的文档去说明。
InnoDB聚簇索引的背景
在InnoDB索引页的物理结构中,“我”讲述了”InnoDB中一切都是索引“。这意味着每个InnoDB引擎的表必须有一个“聚簇索引”,通常是主键。在手册中聚簇索引和第二索引有说:
如果表中没有主键或者一个合适的的唯一索引,InnoDB内部会以一个包含行ID值的合成列生成一个隐藏的聚簇索引。表中的行是按照InnoDB分配的ID排序的。行ID是一个6字节的字段,随着一个新行的插入单调增加。因此,行ID顺序物理上是插入顺序。
“我”之前假设过这就意味着一个不可见的列将会和自增的实现 使用相同的序列生成代码(它自身也有一些拓展性问题)。然而事实上它们是完全不同的实现方式。
隐式行ID的实现方式
如手册所说,事实上是这么实现的:如果一个表没有申明主键和一个不为null的唯一索引,InnoDB将会自动增加一个6字节(48位)的整数列,被叫做行ID,聚集数据都是依靠这列的。这列既不能通过任何查询获取到也不能做像基于行复制的任何内部操作。
手册上没提及的是:所有的表将使用行ID列共享相同的全局计数序列(手册上说“单调增长”并且没有阐明),这是数据字典的一部分。所有行ID可用的最大值(技术上讲,下一个被用到的ID)被储存于系统表空间,在第七页(type SYS),在数据字典的头部(DICT_HDR_ROW_ID字段)。
这个全局的计数权重被dict_sys->mutex保护,甚至是递增(与使用原子增量相反)。 在include / dict0boot.ic处实现(删除了很多空行):
38 UNIV_INLINE
39 row_id_t
40 dict_sys_get_new_row_id(void)
41 /*=========================*/
42 {
43 row_id_t id;
44
45 mutex_enter(&(dict_sys->mutex));
47 id = dict_sys->row_id;
49 if (0 == (id % DICT_HDR_ROW_ID_WRITE_MARGIN)) {
51 dict_hdr_flush_row_id();
52 }
54 dict_sys->row_id++;
56 mutex_exit(&(dict_sys->mutex));
57
58 return(id);
59 }
(你可能也注意到这段代码缺乏对分配给行ID超出的48位的保护。这是不必要草率的编程,但是甚至每秒连续插入1百万(可能有点乐观),也要9年才能耗尽ID空间。“我”猜也是没事的)
确保生成不冲突的ID
计数器每生成256个ID被刷新(以上定义的DICT_HDR_ROW_ID_WRITE_MARGIN),可以修改记录于事务日志的系统数据字典页的值。在启动时,InnoDB将会增加存储于硬盘的DICT_HDR_ROW_ID,至少256,至多511。这样确保生成的任何ID都将小于新的起始值,因此将不会有冲突。
性能和资源冲突的影响
InnoDB中一些其他代码是被dict_sys->mutex保护的,“我”认为任何使用隐式聚簇索引(行ID)的表可以预料到在删(不关联)的表期间经历随机插入的停顿。形成对比地,隐式索引插入到多个表可能会有性能约束,因为将会在共享互斥锁和共享计数变量的缓存争用时序列化。另外,不管事务是否已经提交了(或者未提交),每256个生成的值将会导致系统页修改的写日志和刷新。