How does InnoDB behave without a Primary Key(11.InnoDB在没用主键情况下的行为)

时间:2022-07-24
本文章向大家介绍How does InnoDB behave without a Primary Key(11.InnoDB在没用主键情况下的行为),主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

今天下午,我和Arjen Lentz讨论了InnoDB在没有声明主键的情况下的行为,这个话题很有趣,也没有足够的文档证明,所以有必要写一个简短的帖子。

InnoDB聚集索引的背景

在InnoDB索引页的物理结构中,我描述了“在InnoDB中的任何内容都是索引”。这意味着InnoDB必须为每个表都有一个“聚簇索引”,这通常是主键。手册上说在聚集和二级索引: 如果表没有主键或合适的唯一索引,InnoDB内部会在一个包含行ID值的合成列上生成一个隐藏的聚集索引。这些行是按照InnoDB给表中的行分配的ID排序的。行ID是一个6字节的字段,在插入新行时单调地增加。因此,按行ID排序的行在物理上是按插入顺序排列的。

我之前假设这意味着将使用一个不可见的列和用于实现auto_increment的相同序列生成代码(它本身存在一些可伸缩性问题)。然而,实际上它们是完全不同的实现。

实现隐式的 Row IDs

这实际上是这样实现的,如手册所说,如果一个表声明没有主键和非空的唯一键,InnoDB会自动添加一个6字节(48位)的整数列ROW_ID到表中,并基于该列集群数据。该列不能被任何查询访问,也不能在内部用于任何事情,比如基于行的复制。 手册没有提到的是,所有使用这样的ROW_ID列的表共享相同的全局序列计数器(手册上说“单调递增”,但没有澄清),这是数据字典的一部分。所有行ID的最大使用值(从技术上说,是下一个要使用的ID)存储在第7页(类型SYS)的系统表空间(例如ibdata1)中,在数据字典头(字段DICT_HDR_ROW_ID)中。 这个全局序列计数器由dict_sys->互斥锁保护,甚至用于递增(与使用原子递增相反)。实现在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位溢出。这是不必要的冗余代码,但即使是连续每秒100万次插入(这可能有点乐观;),也需要大约9年的时间来耗尽ID空间。我想应该是这样吧。

确保生成不冲突的id

计数器每生成第256个ID(上面定义的DICT_HDR_ROW_ID_WRITE_MARGIN)就刷新到磁盘,方法是修改SYS数据字典页面中的值,该值被记录到事务日志中。在启动时,InnoDB会将存储在磁盘上的DICT_HDR_ROW_ID增加至少256,最多511。这确保了生成的任何id都小于新的起始值,因此不会有任何冲突。

性能和竞争的影响

鉴于InnoDB中的其他代码受到了dict_sys->互斥锁的保护,我认为可以公平地说,任何具有隐式集群键(ROW_ID)的表都可能在删除(不相关的)表的操作中都会遇到插入卡顿。在具有隐式键的多个表中并行插入可能会受到性能限制,因为它将在共享互斥锁和共享计数器变量的缓存争用上序列化。此外,每生成第256个值,就会导致对SYS页面修改进行一次日志写入(和刷新),而不管事务是否已经提交(或将要提交)。