問題
記得剛接手項目時,看了下項目里的庫表結(jié)構(gòu),發(fā)現(xiàn)表主鍵都是varchar(36),就問研發(fā),為啥搞個這么長的主鍵?研發(fā)說這個主鍵是通過java 程序產(chǎn)生的UUID,考慮到以后數(shù)據(jù)分片不會產(chǎn)生重復。我頓時無語,考慮的還是挺周到,沒有意識到這么長字段做主鍵的問題:
- 首先這個主鍵很長,而主鍵會自動添加在每一二級索引的后面,這樣會導致數(shù)據(jù)冗余嚴重,特別是二級索引建多了的情況下,有時會導致索引的數(shù)據(jù)比表的數(shù)據(jù)還大!
- 其次,這樣產(chǎn)生UUID是無序的,導致插入性能很差。因為當插入一個數(shù)據(jù)到B+tree時,系統(tǒng)將這個數(shù)據(jù)插入到一個合適的位置,這會導致多次IO,而如果數(shù)據(jù)本身是自增的,那么系統(tǒng)只須將它添加的B+Tree的末端即可,效率要高的多。
因此我還是建議使用整型+auto_increment, 雖然MySQLauto_increment有個眾所周知的、歷史悠久的bug(這個bug終于在MySQL8的到修復,MySQL8中auto_increment將會被寫入redo log 中啦),但大多數(shù)情況下還是可靠的,并且MySQL官方也建議使用auto_incremnet做主鍵。但它也有問題,就是無法適應數(shù)據(jù)分片的擴容(分庫分表),除非你之前規(guī)劃的非常好。
MySQL8中的解決方案
如今MySQL8 中對UUID提供了增強性支持,不僅使其長度大大縮小,而且解決了順序的問題?。ㄎ抑苑Q之為增強性支持,是因為他們提供了兩個內(nèi)置函數(shù)做了轉(zhuǎn)換,感覺這種方式有點兒low,這樣的函數(shù)我們也可以寫?。?/p>
大家都知道,UUID是由32位16進制字符串組成(不算分隔符'-')如:
62ab1547-710f-11e8-9a58-5254007205d6
如果直接保存,則需要32個字符,utf8編碼下占用96個字節(jié),對于主鍵來說還是太長。幸運的是UUID中的每個字符都是16進制字符,兩個16進制字符占用一個字節(jié),這樣可以輕松將UUID轉(zhuǎn)換為binary(16),占用16個字節(jié),所需空間大大減少,而且二進制字符串檢索對比效率很高。
但還有一個至關重要的問題是UUID的組成中將timestamp 部分的低位時間段(如毫秒)放在了前面,高位時間段(如年月日)放在了后面,這會導致前面的字符變化很快,后面的變化很慢,從而使產(chǎn)生的UUID不能順序自增。這會導致索引插入效率大大降低。
為解決這一問題,mysql8提供了兩個函數(shù):UID_TO_BIN(arg1)/ BIN_TO_UUID(arg1,arg2)
- UID_TO_BIN(arg1) 將UUID轉(zhuǎn)化為16位二進制字符串,如果參數(shù)arg1為true則將UUID中的timestamp部分中的time-low(第一段字符)和time-high(第三段)調(diào)換,這樣產(chǎn)生的UUID是順序遞增。
- BIN_TO_UUID(arg1,arg2)將16位進制字符串轉(zhuǎn)化為可讀的UUID,arg1為16位二進制字符串,如果arg2省略或為false,即將二進制字符串原位轉(zhuǎn)換;如果arg2為true,則將原來調(diào)換的time-low和time-high再調(diào)換回去,返回原本的uuid.
測試
創(chuàng)建一張表如下:
mysql8[test]>create table t (id varbinary(16) primary key,create_time timestamp default current_timestamp());
Query OK, 0 rows affected (0.34 sec)
插入幾條數(shù)據(jù),注意使用了函數(shù)uuid_to_bin:
mysql8[test]>insert into t (id)values(uuid_to_bin(uuid(),true));
Query OK, 1 row affected (0.08 sec)
mysql8[test]>insert into t (id)values(uuid_to_bin(uuid(),true));
Query OK, 1 row affected (0.08 sec)
mysql8[test]>insert into t (id)values(uuid_to_bin(uuid(),true));
Query OK, 1 row affected (0.07 sec)
查看結(jié)果:
mysql8[test]>select bin_to_uuid(id) id1,bin_to_uuid(id,true) id2, create_time from t;
+--------------------------------------+--------------------------------------+---------------------+
| id1 | id2 | create_time |
+--------------------------------------+--------------------------------------+---------------------+
| 11e87113-f079-024e-8405-5254004332fa | f079024e-7113-11e8-8405-5254004332fa | 2018-06-16 11:18:28 |
| 11e87113-f826-4134-8405-5254004332fa | f8264134-7113-11e8-8405-5254004332fa | 2018-06-16 11:18:41 |
| 11e87113-f88c-c8a6-8405-5254004332fa | f88cc8a6-7113-11e8-8405-5254004332fa | 2018-06-16 11:18:42 |
+--------------------------------------+--------------------------------------+---------------------+
3 rows in set (0.00 sec)
注意字段id1使用了函數(shù)bin_to_uuid(id), 而id2使用了bin_to_uuid(id,true),注意他們在結(jié)果集中的區(qū)別。
如果需要按主鍵查詢,還是需要使用對應的uuid函數(shù):
mysql8[test]>select * from t where id=uuid_to_bin('f079024e-7113-11e8-8405-5254004332fa',true);
+------------------+---------------------+
| id | create_time |
+------------------+---------------------+
| 簍??RT C2?? | 2018-06-16 11:18:28 |
+------------------+---------------------+
1 row in set (0.00 sec)
總結(jié)
MySQL8通過提供兩個內(nèi)置函數(shù)解決了傳統(tǒng)UUID作為主鍵的缺陷,也使UUID成為了設計主鍵的首選,特別是在數(shù)據(jù)分片的架構(gòu)中,其優(yōu)勢十分突出。我當時一看到這個,還很興奮,但UUID終究還不是一種數(shù)據(jù)類型,這多少給應用上增加了些麻煩。相信在MySQL的未來版本會出現(xiàn)一種數(shù)據(jù)類型叫UUID , 總不能一直弄兩個函數(shù)糊弄人吧??。ㄆ鋵嵞壳肮俜揭蔡岬搅藢崿F(xiàn)一種新的數(shù)據(jù)類型,但增加一種數(shù)據(jù)類型屬于重量級實現(xiàn),代碼變動太大,所以當前選擇了增加兩個函數(shù))