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