在使用InnoDB存儲引擎時,如果沒有特別的需要,請永遠使用一個與業務無關的自增字段作為主鍵,除非高并發寫入操作可能需要衡量自增主鍵或有業務安全性要求,后面會講。
經??吹接刑踊虿┛陀懻撝麈I選擇問題,有人建議使用業務無關的自增主鍵,有人覺得沒有必要,完全可以使用如學號或身份證號這種唯一字段作為主鍵。不論支持哪種論點,大多數論據都是業務層面的。如果從數據庫索引優化角度看,使用InnoDB引擎而不使用自增主鍵絕對是一個糟糕的主意。下面從各個方面來討論一下。
一、首先不管主鍵策略是什么,這兩點都是必須遵守的。
1. 主鍵不可修改
對于數據庫來說,主鍵其實是可以修改的,只要不和其他主鍵沖突就可以。但是,對于應用來說,如果一條記錄要修改主鍵,那就會出大問題。
因為主鍵的第二個作用是讓其他表的外鍵引用自己,從而實現關系結構。一旦某個表的主鍵發生了變化,就會導致所有引用了該表的數據必須全部修改外鍵。很多Web應用的數據庫并不是強約束(僅僅引用主鍵但并沒有設置外鍵約束),修改主鍵會導致數據完整性直接被破壞。
2. 業務字段不可用于主鍵
所有涉及到業務的字段,無論它看上去是否唯一,都決不能用作主鍵。例如,用戶表的Email字段是唯一的,但是,如果用它作主鍵,就會導致其他表到處引用Email字段,從而泄露用戶信息。
此外,修改Email實際上是一個業務操作,這個操作就直接違反了上一條原則。
那么,主鍵應該使用哪個字段呢?
主鍵必須使用單獨的,完全沒有業務含義的字段,也就是主鍵本身除了唯一標識和不可修改這兩個責任外,主鍵沒有任何業務含義。
類似的,看上去唯一的用戶名、身份證號等,也不能用作主鍵。對這些唯一字段,應該加上unique索引約束。
二、主鍵應該用什么類型?
上面說了,不考慮業務,從數據庫索引優化角度看,使用InnoDB引擎而不使用自增主鍵絕對是一個糟糕的主意。
下面先簡單說說MySQL索引實現。在MySQL中,索引屬于存儲引擎級別的概念,不同存儲引擎對索引的實現方式是不同的,本文主要討論MyISAM和InnoDB兩個存儲引擎的索引實現方式。
2.1 MyISAM存儲引擎
MyISAM引擎使用B+Tree作為索引結構,葉節點的data域存放的是數據記錄的地址。下圖是MyISAM索引的原理圖:
這里設表一共有三列,假設我們以Col1為主鍵,則上圖是一個MyISAM表的主索引(Primary key)示意??梢钥闯鯩yISAM的索引文件僅僅保存數據記錄的地址。在MyISAM中,主索引和輔助索引(Secondary key)在結構上沒有任何區別,只是主索引要求key是唯一的,而輔助索引的key可以重復。如果我們在Col2上建立一個輔助索引,則此索引的結構如下圖所示:
同樣也是一顆B+Tree,data域保存數據記錄的地址。因此,MyISAM中索引檢索的算法為首先按照B+Tree搜索算法搜索索引,如果指定的Key存在,則取出其data域的值,然后以data域的值為地址,讀取相應數據記錄。
MyISAM的索引方式也叫做“非聚集”的,之所以這么稱呼是為了與InnoDB的聚集索引區分。
2.2 InnoDB存儲引擎
雖然InnoDB也使用B+Tree作為索引結構,但具體實現方式卻與MyISAM截然不同。
第一個重大區別是InnoDB的數據文件本身就是索引文件。從上文知道,MyISAM索引文件和數據文件是分離的,索引文件僅保存數據記錄的地址。而在InnoDB中,表數據文件本身就是按B+Tree組織的一個索引結構,這棵樹的葉節點data域保存了完整的數據記錄。這個索引的key是數據表的主鍵,因此InnoDB表數據文件本身就是主索引。
是InnoDB主索引(同時也是數據文件)的示意圖,可以看到葉節點包含了完整的數據記錄。這種索引叫做聚集索引。因為InnoDB的數據文件本身要按主鍵聚集,所以InnoDB要求表必須有主鍵(MyISAM可以沒有),如果沒有顯式指定,則MySQL系統會自動選擇一個可以唯一標識數據記錄的列作為主鍵,如果不存在這種列,則MySQL自動為InnoDB表生成一個隱含字段作為主鍵,這個字段長度為6個字節,類型為長整形。
第二個與MyISAM索引的不同是InnoDB的輔助索引data域存儲相應記錄主鍵的值而不是地址。換句話說,InnoDB的所有輔助索引都引用主鍵作為data域。例如,下圖為定義在Col3上的一個輔助索引:
這里以英文字符的ASCII碼作為比較準則。聚集索引這種實現方式使得按主鍵的搜索十分高效,但是輔助索引搜索需要檢索兩遍索引:首先檢索輔助索引獲得主鍵,然后用主鍵到主索引中檢索獲得記錄。
了解不同存儲引擎的索引實現方式對于正確使用和優化索引都非常有幫助,例如知道了InnoDB的索引實現后,就很容易明白為什么不建議使用過長的字段作為主鍵,因為所有輔助索引都引用主索引,過長的主索引會令輔助索引變得過大。再例如,用非單調的字段作為主鍵在InnoDB中不是個好主意,因為InnoDB數據文件本身是一顆B+Tree,非單調的主鍵會造成在插入新記錄時數據文件為了維持B+Tree的特性而頻繁的分裂調整,十分低效,而使用自增字段作為主鍵則是一個很好的選擇。
2.3 InnoDB自增主鍵
上文討論過InnoDB的索引實現,InnoDB使用聚集索引,數據記錄本身被存于主索引(一顆B+Tree)的葉子節點上。這就要求同一個葉子節點內(大小為一個內存頁或磁盤頁)的各條數據記錄按主鍵順序存放,因此每當有一條新的記錄插入時,MySQL會根據其主鍵將其插入適當的節點和位置,如果頁面達到裝載因子(InnoDB默認為15/16),則開辟一個新的頁(節點)。
如果表使用自增主鍵,那么每次插入新的記錄,記錄就會順序添加到當前索引節點的后續位置,當一頁寫滿,就會自動開辟一個新的頁。如下圖所示:
這樣就會形成一個緊湊的索引結構,近似順序填滿。由于每次插入時也不需要移動已有數據,因此效率很高,也不會增加很多開銷在維護索引上。
如果使用非自增主鍵(如果身份證號或學號等),由于每次插入主鍵的值近似于隨機,因此每次新紀錄都要被插到現有索引頁得中間某個位置:
此時MySQL不得不為了將新記錄插到合適位置而移動數據,甚至目標頁面可能已經被回寫到磁盤上而從緩存中清掉,此時又要從磁盤上讀回來,這增加了很多開銷,同時頻繁的移動、分頁操作造成了大量的碎片,得到了不夠緊湊的索引結構,后續不得不通過OPTIMIZE TABLE來重建表并優化填充頁面。
因此,只要可以,請盡量在InnoDB上采用自增字段做主鍵。
三、主鍵自增帶來的劣勢是什么?
3.1 自增鎖
對于高并發工作負載,在InnoDB中按主鍵順序插入可能會造成明顯的爭用。主鍵上界會成為”熱點”,因為所有的插入都發生在這里,所以并發插入可能導致間隙鎖競爭。另一個熱點可能是AUTO_INCREMENT鎖機制:如果遇到這個問題,則可能需要考慮重新設計表或者應用,或者更改innodb_autoinc_lock_mode配置。
自增長在數據庫中是非常常見的一種屬性,也是很多DBA或開發人員首選的主鍵方式。在InnoDB存儲引擎的內存結構中,對每個含有自增長值的表都有一個自增長計數器。當對含有自增長的計數器的表進行插入操作時,這個計數器會被初始化,執行如下的語句來得到計數器的值:
1select max(auto_inc_col) from t for update;
插入操作會依據這個自增長的計數器值加1賦予自增長列。這個實現方式稱為AUTO-INC Locking。這種鎖其實是采用一種特殊的表鎖機制,為了提高插入的性能,鎖不是在一個事務完成后才釋放,而是在完成對自增長值插入的SQL語句后立即釋放。
雖然AUTO-INC Locking從一定程度上提高了并發插入的效率,但還是存在一些性能上的問題。首先,對于有自增長值的列的并發插入性能較差,事務必須等待前一個插入的完成,雖然不用等待事務的完成。其次,對于INSERT….SELECT的大數據的插入會影響插入的性能,因為另一個事務中的插入會被阻塞。
從MySQL 5.1.22版本開始,InnoDB存儲引擎中提供了一種輕量級互斥量的自增長實現機制,這種機制大大提高了自增長值插入的性能。并且從該版本開始,InnoDB存儲引擎提供了一個參數innodb_autoinc_lock_mode來控制自增長的模式,該參數的默認值為1。在繼續討論新的自增長實現方式之前,需要對自增長的插入進行分類。如下說明:
insert-like:指所有的插入語句,如INSERT、REPLACE、INSERT…SELECT,REPLACE…SELECT、LOAD DATA等。
simple inserts:指能在插入前就確定插入行數的語句,這些語句包括INSERT、REPLACE等。需要注意的是:simple inserts不包含INSERT…ON DUPLICATE KEY UPDATE這類SQL語句。
bulk inserts:指在插入前不能確定得到插入行數的語句,如INSERT…SELECT,REPLACE…SELECT,LOAD DATA。
mixed-mode inserts:指插入中有一部分的值是自增長的,有一部分是確定的。入INSERT INTO t1(c1,c2) VALUES(1,’a’),(2,’a’),(3,’a’);也可以是指INSERT…ON DUPLICATE KEY UPDATE這類SQL語句。
接下來分析參數innodb_autoinc_lock_mode以及各個設置下對自增長的影響,其總共有三個有效值可供設定,即0、1、2,具體說明如下:
0:這是MySQL 5.1.22版本之前自增長的實現方式,即通過表鎖的AUTO-INC Locking方式,因為有了新的自增長實現方式,0這個選項不應該是新版用戶的首選了。
1:這是該參數的默認值,對于”simple inserts”,該值會用互斥量(mutex)去對內存中的計數器進行累加的操作。對于”bulk inserts”,還是使用傳統表鎖的AUTO-INC Locking方式。在這種配置下,如果不考慮回滾操作,對于自增值列的增長還是連續的。并且在這種方式下,statement-based方式的replication還是能很好地工作。需要注意的是,如果已經使用AUTO-INC Locing方式去產生自增長的值,而這時需要再進行”simple inserts”的操作時,還是需要等待AUTO-INC Locking的釋放。
2:在這個模式下,對于所有”INSERT-LIKE”自增長值的產生都是通過互斥量,而不是AUTO-INC Locking的方式。顯然,這是性能最高的方式。然而,這會帶來一定的問題,因為并發插入的存在,在每次插入時,自增長的值可能不是連續的。此外,最重要的是,基于Statement-Base Replication會出現問題。因此,使用這個模式,任何時候都應該使用row-base replication。這樣才能保證最大的并發性能及replication主從數據的一致。
這里需要特別注意,InnoDB跟MyISAM不同,MyISAM存儲引擎是表鎖設計,自增長不用考慮并發插入的問題。因此在master上用InnoDB存儲引擎,在slave上用MyISAM存儲引擎的replication架構下,用戶可以考慮這種情況。
另外,InnoDB存儲引擎,自增持列必須是索引,同時必須是索引的第一個列,如果不是第一個列,會拋出異常,而MyiSAM不會有這個問題。
mysql> create table test(id int primary key not null,count int auto_increment not null);
ERROR 1075 (42000): Incorrect table definition; there can be only one auto column and it must be defined as a key
3.2 無法水平切分
雖然使用自增主鍵后,無法做水平切分。但是數據庫自增最大的問題還不在于數據庫單點造成無法水平切分,因為絕大部分公司還撐不到業務需要分庫的情況就倒閉了。
3.3 業務安全性
自增主鍵最大的問題是把公司業務的關鍵運營數據完全暴露給了競爭對手和VC。舉個例子,用戶表采用自增主鍵,只需要每周一早上去注冊一個用戶,把上周注冊的ID和本周注冊的ID一比,立刻就知道了該公司一周的新增用戶數量。如果網站聲稱新增了10萬用戶,但ID卻只增加了1千,就只能呵呵了。
因為主鍵的本質是保證唯一記錄,并不要求主鍵是連續的。實際上不連續的更好,這樣既避免了運營數據泄露,也給黑客預測ID制造了障礙,具有更高的安全性。
用字符串主鍵就不存在這個問題。如果我們用一個UUID作為主鍵,即varchar(32),除了占用的存儲空間較多外,字符串主鍵具有不可預測性。
有人覺得UUID完全隨機,主鍵本身沒有按時間遞增,不利于直接主鍵排序。其實解決這個問題很簡單。
方法一,直接用時間戳+UUID構造一個主鍵,時間戳注意補0,這樣生成的主鍵就是按時間排序的。這個方法簡單粗暴,缺點是主鍵更長了。
方法二,自定義一個算法,時間戳放高位,序列號放低位,還可以保留機器位,然后用base32編碼,可以把長度控制在20個字符內。
有人會問,根據方法二,構造包含時間戳和序列號的64位整數作為主鍵是否可行?
理論上來說是可行的,因為時間戳0xffffffff可以表示到2100年。但是剩下的位不是ffffffff而是只有fffff,如果給機器分配ff作為標識,那么每秒只能最多生成0xfff+1=4096個主鍵,對一些大型應用不太夠用。
為啥64位整數除掉時間戳只能用后面的fffff位呢?這是因為JavaScript的Number類型是56位精度,它能表示的最大整數是0x1fffffffffffff,而我們遲早會用REST跟JavaScript打交道,所以要把64位整數的范圍限制在0x1fffffffffffff內,否則與JavaScript交互就會出錯。
雖然理論上64位整數做時間戳+序列號的主鍵是沒問題的,但是實踐中是沒法繞開與JavaScript交互的,綜合考慮,字符串主鍵最可靠。
轉自:http://www.ywnds.com/?p=8735