在使用InnoDB存儲引擎時,如果沒有特別的需要蛮穿,請永遠(yuǎn)使用一個與業(yè)務(wù)無關(guān)的自增字段作為主鍵庶骄,除非高并發(fā)寫入操作可能需要衡量自增主鍵或有業(yè)務(wù)安全性要求,后面會講践磅。
經(jīng)常看到有帖子或博客討論主鍵選擇問題灸异,有人建議使用業(yè)務(wù)無關(guān)的自增主鍵府适,有人覺得沒有必要羔飞,完全可以使用如學(xué)號或身份證號這種唯一字段作為主鍵。不論支持哪種論點(diǎn)檐春,大多數(shù)論據(jù)都是業(yè)務(wù)層面的逻淌。如果從數(shù)據(jù)庫索引優(yōu)化角度看,使用InnoDB引擎而不使用自增主鍵絕對是一個糟糕的主意疟暖。下面從各個方面來討論一下卡儒。
一、首先不管主鍵策略是什么俐巴,這兩點(diǎn)都是必須遵守的骨望。
1. 主鍵不可修改
對于數(shù)據(jù)庫來說,主鍵其實(shí)是可以修改的欣舵,只要不和其他主鍵沖突就可以擎鸠。但是,對于應(yīng)用來說缘圈,如果一條記錄要修改主鍵劣光,那就會出大問題。
因?yàn)橹麈I的第二個作用是讓其他表的外鍵引用自己糟把,從而實(shí)現(xiàn)關(guān)系結(jié)構(gòu)绢涡。一旦某個表的主鍵發(fā)生了變化,就會導(dǎo)致所有引用了該表的數(shù)據(jù)必須全部修改外鍵遣疯。很多Web應(yīng)用的數(shù)據(jù)庫并不是強(qiáng)約束(僅僅引用主鍵但并沒有設(shè)置外鍵約束)垂寥,修改主鍵會導(dǎo)致數(shù)據(jù)完整性直接被破壞。
2. 業(yè)務(wù)字段不可用于主鍵
所有涉及到業(yè)務(wù)的字段另锋,無論它看上去是否唯一滞项,都決不能用作主鍵。例如夭坪,用戶表的Email字段是唯一的文判,但是,如果用它作主鍵室梅,就會導(dǎo)致其他表到處引用Email字段戏仓,從而泄露用戶信息。
此外亡鼠,修改Email實(shí)際上是一個業(yè)務(wù)操作赏殃,這個操作就直接違反了上一條原則。
那么间涵,主鍵應(yīng)該使用哪個字段呢仁热?
主鍵必須使用單獨(dú)的,完全沒有業(yè)務(wù)含義的字段勾哩,也就是主鍵本身除了唯一標(biāo)識和不可修改這兩個責(zé)任外抗蠢,主鍵沒有任何業(yè)務(wù)含義举哟。
類似的,看上去唯一的用戶名迅矛、身份證號等妨猩,也不能用作主鍵。對這些唯一字段秽褒,應(yīng)該加上unique索引約束壶硅。
二、主鍵應(yīng)該用什么類型销斟?
上面說了庐椒,不考慮業(yè)務(wù),從數(shù)據(jù)庫索引優(yōu)化角度看票堵,使用InnoDB引擎而不使用自增主鍵絕對是一個糟糕的主意扼睬。
下面先簡單說說MySQL索引實(shí)現(xiàn)。在MySQL中悴势,索引屬于存儲引擎級別的概念窗宇,不同存儲引擎對索引的實(shí)現(xiàn)方式是不同的,本文主要討論MyISAM和InnoDB兩個存儲引擎的索引實(shí)現(xiàn)方式特纤。
2.1 MyISAM存儲引擎
MyISAM引擎使用B+Tree作為索引結(jié)構(gòu)军俊,葉節(jié)點(diǎn)的data域存放的是數(shù)據(jù)記錄的地址。下圖是MyISAM索引的原理圖:
這里設(shè)表一共有三列捧存,假設(shè)我們以Col1為主鍵粪躬,則上圖是一個MyISAM表的主索引(Primary key)示意∥粞ǎ可以看出MyISAM的索引文件僅僅保存數(shù)據(jù)記錄的地址镰官。在MyISAM中,主索引和輔助索引(Secondary key)在結(jié)構(gòu)上沒有任何區(qū)別吗货,只是主索引要求key是唯一的泳唠,而輔助索引的key可以重復(fù)。如果我們在Col2上建立一個輔助索引宙搬,則此索引的結(jié)構(gòu)如下圖所示:
同樣也是一顆B+Tree笨腥,data域保存數(shù)據(jù)記錄的地址。因此勇垛,MyISAM中索引檢索的算法為首先按照B+Tree搜索算法搜索索引脖母,如果指定的Key存在,則取出其data域的值闲孤,然后以data域的值為地址谆级,讀取相應(yīng)數(shù)據(jù)記錄。
MyISAM的索引方式也叫做“非聚集”的,之所以這么稱呼是為了與InnoDB的聚集索引區(qū)分哨苛。
2.2 InnoDB存儲引擎
雖然InnoDB也使用B+Tree作為索引結(jié)構(gòu)鸽凶,但具體實(shí)現(xiàn)方式卻與MyISAM截然不同币砂。
第一個重大區(qū)別是InnoDB的數(shù)據(jù)文件本身就是索引文件建峭。從上文知道,MyISAM索引文件和數(shù)據(jù)文件是分離的决摧,索引文件僅保存數(shù)據(jù)記錄的地址亿蒸。而在InnoDB中,表數(shù)據(jù)文件本身就是按B+Tree組織的一個索引結(jié)構(gòu)掌桩,這棵樹的葉節(jié)點(diǎn)data域保存了完整的數(shù)據(jù)記錄边锁。這個索引的key是數(shù)據(jù)表的主鍵,因此InnoDB表數(shù)據(jù)文件本身就是主索引波岛。
是InnoDB主索引(同時也是數(shù)據(jù)文件)的示意圖茅坛,可以看到葉節(jié)點(diǎn)包含了完整的數(shù)據(jù)記錄。這種索引叫做聚集索引则拷。因?yàn)镮nnoDB的數(shù)據(jù)文件本身要按主鍵聚集贡蓖,所以InnoDB要求表必須有主鍵(MyISAM可以沒有),如果沒有顯式指定煌茬,則MySQL系統(tǒng)會自動選擇一個可以唯一標(biāo)識數(shù)據(jù)記錄的列作為主鍵斥铺,如果不存在這種列,則MySQL自動為InnoDB表生成一個隱含字段作為主鍵坛善,這個字段長度為6個字節(jié)晾蜘,類型為長整形。
第二個與MyISAM索引的不同是InnoDB的輔助索引data域存儲相應(yīng)記錄主鍵的值而不是地址眠屎。換句話說剔交,InnoDB的所有輔助索引都引用主鍵作為data域。例如改衩,下圖為定義在Col3上的一個輔助索引:
這里以英文字符的ASCII碼作為比較準(zhǔn)則岖常。聚集索引這種實(shí)現(xiàn)方式使得按主鍵的搜索十分高效,但是輔助索引搜索需要檢索兩遍索引:首先檢索輔助索引獲得主鍵燎字,然后用主鍵到主索引中檢索獲得記錄腥椒。
了解不同存儲引擎的索引實(shí)現(xiàn)方式對于正確使用和優(yōu)化索引都非常有幫助,例如知道了InnoDB的索引實(shí)現(xiàn)后候衍,就很容易明白為什么不建議使用過長的字段作為主鍵笼蛛,因?yàn)樗休o助索引都引用主索引,過長的主索引會令輔助索引變得過大蛉鹿。再例如滨砍,用非單調(diào)的字段作為主鍵在InnoDB中不是個好主意,因?yàn)镮nnoDB數(shù)據(jù)文件本身是一顆B+Tree,非單調(diào)的主鍵會造成在插入新記錄時數(shù)據(jù)文件為了維持B+Tree的特性而頻繁的分裂調(diào)整惋戏,十分低效领追,而使用自增字段作為主鍵則是一個很好的選擇。
2.3 InnoDB自增主鍵
上文討論過InnoDB的索引實(shí)現(xiàn)响逢,InnoDB使用聚集索引绒窑,數(shù)據(jù)記錄本身被存于主索引(一顆B+Tree)的葉子節(jié)點(diǎn)上。這就要求同一個葉子節(jié)點(diǎn)內(nèi)(大小為一個內(nèi)存頁或磁盤頁)的各條數(shù)據(jù)記錄按主鍵順序存放舔亭,因此每當(dāng)有一條新的記錄插入時些膨,MySQL會根據(jù)其主鍵將其插入適當(dāng)?shù)墓?jié)點(diǎn)和位置,如果頁面達(dá)到裝載因子(InnoDB默認(rèn)為15/16)钦铺,則開辟一個新的頁(節(jié)點(diǎn))订雾。
如果表使用自增主鍵,那么每次插入新的記錄矛洞,記錄就會順序添加到當(dāng)前索引節(jié)點(diǎn)的后續(xù)位置洼哎,當(dāng)一頁寫滿,就會自動開辟一個新的頁沼本。如下圖所示:
這樣就會形成一個緊湊的索引結(jié)構(gòu)噩峦,近似順序填滿。由于每次插入時也不需要移動已有數(shù)據(jù)擅威,因此效率很高壕探,也不會增加很多開銷在維護(hù)索引上。
如果使用非自增主鍵(如果身份證號或?qū)W號等)郊丛,由于每次插入主鍵的值近似于隨機(jī)李请,因此每次新紀(jì)錄都要被插到現(xiàn)有索引頁得中間某個位置:
此時MySQL不得不為了將新記錄插到合適位置而移動數(shù)據(jù),甚至目標(biāo)頁面可能已經(jīng)被回寫到磁盤上而從緩存中清掉厉熟,此時又要從磁盤上讀回來导盅,這增加了很多開銷,同時頻繁的移動揍瑟、分頁操作造成了大量的碎片白翻,得到了不夠緊湊的索引結(jié)構(gòu),后續(xù)不得不通過OPTIMIZE TABLE來重建表并優(yōu)化填充頁面绢片。
因此滤馍,只要可以,請盡量在InnoDB上采用自增字段做主鍵底循。
三巢株、主鍵自增帶來的劣勢是什么?
3.1 自增鎖
對于高并發(fā)工作負(fù)載熙涤,在InnoDB中按主鍵順序插入可能會造成明顯的爭用阁苞。主鍵上界會成為”熱點(diǎn)”困檩,因?yàn)樗械牟迦攵及l(fā)生在這里,所以并發(fā)插入可能導(dǎo)致間隙鎖競爭那槽。另一個熱點(diǎn)可能是AUTO_INCREMENT鎖機(jī)制:如果遇到這個問題悼沿,則可能需要考慮重新設(shè)計(jì)表或者應(yīng)用,或者更改innodb_autoinc_lock_mode配置骚灸。
自增長在數(shù)據(jù)庫中是非常常見的一種屬性糟趾,也是很多DBA或開發(fā)人員首選的主鍵方式。在InnoDB存儲引擎的內(nèi)存結(jié)構(gòu)中逢唤,對每個含有自增長值的表都有一個自增長計(jì)數(shù)器拉讯。當(dāng)對含有自增長的計(jì)數(shù)器的表進(jìn)行插入操作時涤浇,這個計(jì)數(shù)器會被初始化鳖藕,執(zhí)行如下的語句來得到計(jì)數(shù)器的值:
1select max(auto_inc_col) from t for update;
插入操作會依據(jù)這個自增長的計(jì)數(shù)器值加1賦予自增長列。這個實(shí)現(xiàn)方式稱為AUTO-INC Locking只锭。這種鎖其實(shí)是采用一種特殊的表鎖機(jī)制著恩,為了提高插入的性能,鎖不是在一個事務(wù)完成后才釋放蜻展,而是在完成對自增長值插入的SQL語句后立即釋放喉誊。
雖然AUTO-INC Locking從一定程度上提高了并發(fā)插入的效率,但還是存在一些性能上的問題纵顾。首先伍茄,對于有自增長值的列的并發(fā)插入性能較差,事務(wù)必須等待前一個插入的完成施逾,雖然不用等待事務(wù)的完成敷矫。其次,對于INSERT….SELECT的大數(shù)據(jù)的插入會影響插入的性能汉额,因?yàn)榱硪粋€事務(wù)中的插入會被阻塞曹仗。
從MySQL 5.1.22版本開始,InnoDB存儲引擎中提供了一種輕量級互斥量的自增長實(shí)現(xiàn)機(jī)制蠕搜,這種機(jī)制大大提高了自增長值插入的性能怎茫。并且從該版本開始,InnoDB存儲引擎提供了一個參數(shù)innodb_autoinc_lock_mode來控制自增長的模式妓灌,該參數(shù)的默認(rèn)值為1轨蛤。在繼續(xù)討論新的自增長實(shí)現(xiàn)方式之前,需要對自增長的插入進(jìn)行分類虫埂。如下說明:
insert-like:指所有的插入語句祥山,如INSERT、REPLACE告丢、INSERT…SELECT枪蘑,REPLACE…SELECT损谦、LOAD DATA等。
simple inserts:指能在插入前就確定插入行數(shù)的語句岳颇,這些語句包括INSERT照捡、REPLACE等。需要注意的是:simple inserts不包含INSERT…ON DUPLICATE KEY UPDATE這類SQL語句话侧。
bulk inserts:指在插入前不能確定得到插入行數(shù)的語句栗精,如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語句。
接下來分析參數(shù)innodb_autoinc_lock_mode以及各個設(shè)置下對自增長的影響赫悄,其總共有三個有效值可供設(shè)定原献,即0、1埂淮、2姑隅,具體說明如下:
0:這是MySQL 5.1.22版本之前自增長的實(shí)現(xiàn)方式,即通過表鎖的AUTO-INC Locking方式倔撞,因?yàn)橛辛诵碌淖栽鲩L實(shí)現(xiàn)方式讲仰,0這個選項(xiàng)不應(yīng)該是新版用戶的首選了。
1:這是該參數(shù)的默認(rèn)值痪蝇,對于”simple inserts”鄙陡,該值會用互斥量(mutex)去對內(nèi)存中的計(jì)數(shù)器進(jìn)行累加的操作。對于”bulk inserts”霹俺,還是使用傳統(tǒng)表鎖的AUTO-INC Locking方式柔吼。在這種配置下,如果不考慮回滾操作丙唧,對于自增值列的增長還是連續(xù)的愈魏。并且在這種方式下,statement-based方式的replication還是能很好地工作想际。需要注意的是培漏,如果已經(jīng)使用AUTO-INC Locing方式去產(chǎn)生自增長的值,而這時需要再進(jìn)行”simple inserts”的操作時胡本,還是需要等待AUTO-INC Locking的釋放牌柄。
2:在這個模式下,對于所有”INSERT-LIKE”自增長值的產(chǎn)生都是通過互斥量侧甫,而不是AUTO-INC Locking的方式珊佣。顯然蹋宦,這是性能最高的方式。然而咒锻,這會帶來一定的問題冷冗,因?yàn)椴l(fā)插入的存在,在每次插入時惑艇,自增長的值可能不是連續(xù)的蒿辙。此外,最重要的是滨巴,基于Statement-Base Replication會出現(xiàn)問題思灌。因此,使用這個模式恭取,任何時候都應(yīng)該使用row-base replication泰偿。這樣才能保證最大的并發(fā)性能及replication主從數(shù)據(jù)的一致。
這里需要特別注意秽荤,InnoDB跟MyISAM不同甜奄,MyISAM存儲引擎是表鎖設(shè)計(jì),自增長不用考慮并發(fā)插入的問題窃款。因此在master上用InnoDB存儲引擎,在slave上用MyISAM存儲引擎的replication架構(gòu)下牍氛,用戶可以考慮這種情況晨继。
另外,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 無法水平切分
雖然使用自增主鍵后玩祟,無法做水平切分腹缩。但是數(shù)據(jù)庫自增最大的問題還不在于數(shù)據(jù)庫單點(diǎn)造成無法水平切分,因?yàn)榻^大部分公司還撐不到業(yè)務(wù)需要分庫的情況就倒閉了空扎。
3.3 業(yè)務(wù)安全性
自增主鍵最大的問題是把公司業(yè)務(wù)的關(guān)鍵運(yùn)營數(shù)據(jù)完全暴露給了競爭對手和VC藏鹊。舉個例子,用戶表采用自增主鍵转锈,只需要每周一早上去注冊一個用戶盘寡,把上周注冊的ID和本周注冊的ID一比,立刻就知道了該公司一周的新增用戶數(shù)量撮慨。如果網(wǎng)站聲稱新增了10萬用戶竿痰,但I(xiàn)D卻只增加了1千脆粥,就只能呵呵了。
因?yàn)橹麈I的本質(zhì)是保證唯一記錄影涉,并不要求主鍵是連續(xù)的冠绢。實(shí)際上不連續(xù)的更好,這樣既避免了運(yùn)營數(shù)據(jù)泄露常潮,也給黑客預(yù)測ID制造了障礙弟胀,具有更高的安全性。
用字符串主鍵就不存在這個問題喊式。如果我們用一個UUID作為主鍵孵户,即varchar(32),除了占用的存儲空間較多外岔留,字符串主鍵具有不可預(yù)測性夏哭。
有人覺得UUID完全隨機(jī),主鍵本身沒有按時間遞增献联,不利于直接主鍵排序竖配。其實(shí)解決這個問題很簡單。
方法一里逆,直接用時間戳+UUID構(gòu)造一個主鍵进胯,時間戳注意補(bǔ)0,這樣生成的主鍵就是按時間排序的原押。這個方法簡單粗暴胁镐,缺點(diǎn)是主鍵更長了。
方法二诸衔,自定義一個算法盯漂,時間戳放高位,序列號放低位笨农,還可以保留機(jī)器位就缆,然后用base32編碼,可以把長度控制在20個字符內(nèi)谒亦。
有人會問竭宰,根據(jù)方法二,構(gòu)造包含時間戳和序列號的64位整數(shù)作為主鍵是否可行诊霹?
理論上來說是可行的羞延,因?yàn)闀r間戳0xffffffff可以表示到2100年。但是剩下的位不是ffffffff而是只有fffff脾还,如果給機(jī)器分配ff作為標(biāo)識伴箩,那么每秒只能最多生成0xfff+1=4096個主鍵,對一些大型應(yīng)用不太夠用鄙漏。
為啥64位整數(shù)除掉時間戳只能用后面的fffff位呢嗤谚?這是因?yàn)镴avaScript的Number類型是56位精度棺蛛,它能表示的最大整數(shù)是0x1fffffffffffff,而我們遲早會用REST跟JavaScript打交道巩步,所以要把64位整數(shù)的范圍限制在0x1fffffffffffff內(nèi)旁赊,否則與JavaScript交互就會出錯。
雖然理論上64位整數(shù)做時間戳+序列號的主鍵是沒問題的椅野,但是實(shí)踐中是沒法繞開與JavaScript交互的终畅,綜合考慮,字符串主鍵最可靠竟闪。
轉(zhuǎn)自:http://www.ywnds.com/?p=8735