本文章翻譯自 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個生成的值將會導致系統頁修改的寫日志和刷新。
