面試官:主鍵使用自增還是UUID?
引言
大家好,我們又見面了,今天給大家分享的是一道經(jīng)典的面試題:MySQL建表的時候主鍵使用自增還是UUID?
下面讓我們一起來多角度的辯證的探究一下這個問題!
占用空間角度
首先我們來看一下自增id,在MySQL中自增id通常選用int類型或者阿里巴巴手冊推薦的bigint類型,其中int類型占用4個字節(jié),而bigint占用8個字節(jié)。
阿里巴巴手冊之所以推薦使用bigint類型是因為無符號int最大值為2^32-1這個值不到43億,對于阿里的業(yè)務體量這個數(shù)字可能不太夠用。
這時候可能有人就會想到,我們在表數(shù)據(jù)量千萬級左右就已經(jīng)需要考慮分表了,根本不會讓單表數(shù)據(jù)達到43億這個量級,怎么會不夠用呢?但是一旦這個表存在頻繁的插入、刪除操作,那么43億這個自增值也是可能達到的。
不過像一些字典表的情況本身數(shù)據(jù)量就很小,所以這個時候可以不那么死板,大膽的使用int類型的自增id作為主鍵。
接下來我們分析一下UUID,通常情況下UUID由32個字符+4個'-'組成,長度為36字符,即便使用ASCII編碼一個字符也要占用一個字節(jié)。
這么看來在占用空間角度UUID敗給了自增id。
占用空間過大會帶來什么問題呢?
MySQL通過使用頁來組織數(shù)據(jù),每一個頁的大小是固定的,當主鍵占用空間越大,每一個頁存放的主鍵索引就越少,這樣最終就可能導致主鍵索引樹深度變深,從而使得在搜索數(shù)據(jù)的時候發(fā)生的磁盤IO次數(shù)變多。
而我們最常用的Innodb存儲引擎是索引組織表,其他索引樹底層會記錄主鍵值以便回表使用,所以主鍵索引樹變深最終將會影響絕大多數(shù)涉及索引查詢的效率。
插入效率角度
想要探究插入效率問題,就繞不開MySQL最常用的索引底層數(shù)據(jù)結構b+樹了。
網(wǎng)上大多數(shù)文章談到插入效率問題,往往都是:索引樹是有序的,將無序數(shù)據(jù)插入到索引樹中間去,這樣可能會頻繁地導致頁分裂,從而導致性能下降,這樣一筆帶過。
深入探究
當我們通過b+樹操作在線演示網(wǎng)站實踐就會神奇的發(fā)現(xiàn),即使是插入有序數(shù)據(jù)依然會導致頻繁的頁分裂,這是為什么呢?
這是因為b+樹的默認插入算法,考慮的是插入的數(shù)據(jù)是隨機的,定位到每一個葉子結點的概率是均等的,所以在頁分裂的時候采取了對半分的策略。
下圖是一個五階b+樹,順序插入14個數(shù)據(jù)的演示結果。從結果中頁的數(shù)量我們可以看出頻繁的發(fā)生了頁分裂,而且現(xiàn)有頁的空間占用率浪費了近%50。
圖片
如果算法直接拿到MySQL中使用,假設一個MySQL頁可以存儲1000條數(shù)據(jù),那一次分頁后每一個頁只存儲500條數(shù)據(jù),這樣的浪費是不被容忍的。
所以MySQL默認所使用的InnoDB存儲引擎就對插入算法進行了優(yōu)化,通過在Page Header中記錄以下幾個字段:
- PAGE_LAST_INSERT
- PAGE_DIRECTION
- PAGE_N_DIRECTION
InnoDB存儲引擎會通過上述幾個字段動態(tài)分析頁分裂的方向和頁分裂的位置,回歸到我們的問題,當插入的數(shù)據(jù)是有序的為什么會減少頁分裂的發(fā)生?
這是因為當插入的數(shù)據(jù)是自增的,索引的插入模式近似于:葉子結點在發(fā)生分裂時,保持原有的葉子結點不變,將新增的數(shù)據(jù)插入到新的葉子結點,并把這個當前插入值,提升到父結點。
但是如果僅是上述這種模式,又會引出新的漏洞,也就是MySQL比較有名的Bug #67718,詳情見文末參考1官方文檔。
下面我將通過手繪的示意圖為大家簡單介紹,需要注意示意圖中的五階b+樹,僅是為了更方便大家理解,并不代表實際存儲結構。
圖片
通過示意圖我們可以發(fā)現(xiàn),當順序插入數(shù)據(jù)的時候這種算法一切安好,先是鋪滿了頁1,然后分裂出頁2繼續(xù)填充,在這種情況下空間利用率達到了最大,頁分裂出現(xiàn)頻率也降到了最低。
但當不再按照順序插入數(shù)據(jù),而是在已滿的頁1后面追加數(shù)據(jù)18,此刻按照下述規(guī)則新增數(shù)據(jù)就會分裂出頁3。
保持原有的葉子結點不變,將新增的數(shù)據(jù)插入到新的葉子結點,并把這個當前插入值,提升到父結點
再按照同樣規(guī)則追加數(shù)據(jù)16,就會分裂出頁4,顯然這種算法在極端情況下帶來了更嚴重的空間浪費。
官方也很快發(fā)現(xiàn)并解決了這個問題,采用的方法大致是:如果葉子結點滿了之后,如果該葉子結點后面有還有葉子結點,則會將新的數(shù)據(jù)合并到后續(xù)的葉子結點中。
比如在插入數(shù)據(jù)18的時候不會再新增一個分頁,而是將數(shù)據(jù)18插入到頁2中,詳情仍見文末參考1官方文檔,不再展開贅述。
回歸正題
通過上述底層原理分析,其實我們不難發(fā)現(xiàn),InnoDB存儲引擎在設計的時候就更希望我們使用順序值作為主鍵。
這一點在我們建表時沒有指定主鍵,并且在不含有非NULL的唯一鍵的情況下,InnoDB存儲引擎會自動生成一個隱式rowid作為聚簇索引的索引鍵也可以看出。
自增id毋庸置疑肯定是有序的,而UUID因為在設計的過程中將時間低位放置在了時間高位前面,所以生成的數(shù)據(jù)肯定是無序的。
這么看來UUID在插入效率角度也是敗給了自增id
實際應用角度
我們通過在占用空間和插入效率兩個角度進行對比分析,發(fā)現(xiàn)UUID竟然都敗給了自增id,可能這也是阿里巴巴開發(fā)手冊規(guī)定單表的主鍵id必須為無符號的bigint類型,且是自增的原因。
但是在實際應用中我們建表就可以無腦使用自增id了嗎?
其實并不然,以上的分析都是站在數(shù)據(jù)庫的角度進行的分析,強調(diào)了單表。在實際業(yè)務中自增id有很多弊端。
- 安全方面:對外暴露的接口可能會泄露信息。比如一個外賣平臺的訂單查詢接口:/order/1/,訂單編號采用了自增的id,那么完全可以通過在兩個時間分別下一單,然后根據(jù)編號差值就能推算出該平臺該段時間的訂單量,亦或被別有用心者輕易的進行信息爬取。
- 性能方面:自增id需要在數(shù)據(jù)庫服務端進行生成,對性能有損耗。并且自增id需要在插入后才能生成,如后續(xù)業(yè)務需要獲取本次id其實是潛藏著一次與數(shù)據(jù)庫的交互。
- 唯一性方面:對于自增id最大的缺陷其實就是局部唯一性,無法做到在全局任意服務器唯一,這一點使得自增id在分布式時代有些失寵。
而UUID的全局唯一性,恰好與分布式系統(tǒng)更加契合,正因如此在Mysql8.0對UUID進行了一波優(yōu)化。
一定程度上解決UUID存在的空間占用的問題,不僅除去了UUID字符串中無意義的"-"字符串,而且將字符串用二進制類型保存,從而使存儲空間降低至16字節(jié)。
更為重要的是MySQL8.0提供了調(diào)換UUID時間低位和時間高位的方法,使得時間高位在前低位在后,進而將UUID從無序變?yōu)橛行?,大大提高了?shù)據(jù)插入效率。
通過MySQL8.0提供的uuid_to_bin函數(shù)即可實現(xiàn)上述操作。
uuid_to_bin(UUID())可獲取二進制的UUID。
uuid_to_bin(UUID(),TRUE)可以在將UUID轉(zhuǎn)換為二進制的同時,將時間的低位和高位互換,便于排序。
同時MySQL 8.0也提供了BIN_TO_UUID函數(shù),支持將二進制值反轉(zhuǎn)為UUID字符串。
有序性+全局唯一性,這不正是分布式系統(tǒng)主鍵所追求的!
那么16字節(jié)的有序UUID,與8字節(jié)的自增id、原始的UUID相比,性能和占用空間究竟如何呢?
這里直接粘來大佬提供的測試數(shù)據(jù),插入1億條數(shù)據(jù),每條數(shù)據(jù)占用500字節(jié),含有3個二級索引,最終的測試結果如下所示:
圖片
可以看出在效率方面有序的UUID最優(yōu),在占用空間方面略遜于自增id。
總結
通過上述分析我們可以看出無論是自增id還是UUID都各有優(yōu)勢,在實際開發(fā)中,我們可以根據(jù)不同的場景選用不同的主鍵策略。
對于非核心業(yè)務,可以使用對應表的主鍵自增id,例如告警、日志、監(jiān)控等信息。
對于核心業(yè)務,主鍵的設計應該優(yōu)先考慮全局唯一且單調(diào)遞增即分布式id。除非已經(jīng)確定系統(tǒng)一定是單機系統(tǒng),未來也沒有迭代成分布式系統(tǒng)的可能。
對于分布式id目前主流方案包括UUID、雪花算法、MySQL維護自增id表、redis的incr命令、美團技術團隊的Leaf分布式id生成服務等。
在軟件開發(fā)中只有平衡,沒有銀彈!大家可以在實踐中靈活選用,在面試過程中也可以辯證的分析,展現(xiàn)出自己的思考。
今天的分享到此結束,希望大家能夠有所收獲!
參考
1、https://bugs.mysql.com/bug.php?id=67718
2、https://blog.csdn.net/qq_42389764/article/details/108152624
3、https://cloud.tencent.com/developer/article/2163702
4、https://blog.csdn.net/shenchaohao12321/article/details/83243501