UUID的壓縮

概述

UUID,通用唯一識別碼(Universally Unique Identifier)。
UUID的目的是讓分佈式系統中的所有元素都能有唯一的辨識信息,而不需要透過中央控制端來做辨識信息的指定。
UUID的標準型式包含32個16進制數字,以連字號分爲五段,形式爲8-4-4-4-12的32個字符。
示例:

550e8400-e29b-41d4-a716-446655440000

——以上內容摘自百度百科

實現

UUID有很多實現版本,以下是JDK的一個實現:

    private static class Holder {
        static final SecureRandom numberGenerator = new SecureRandom();
    }

    public static UUID randomUUID() {
        SecureRandom ng = Holder.numberGenerator;

        byte[] randomBytes = new byte[16];
        ng.nextBytes(randomBytes);
        randomBytes[6]  &= 0x0f;  /* clear version        */
        randomBytes[6]  |= 0x40;  /* set to version 4     */
        randomBytes[8]  &= 0x3f;  /* clear variant        */
        randomBytes[8]  |= 0x80;  /* set to IETF variant  */
        return new UUID(randomBytes);
    }

用SecureRandom生成的16字節(128bit)隨機數,用掩碼打上版本和IETF標識。
實際有效隨機位122位。關於衝突概率,可以參考筆者另一片文章,漫談散列函數

特徵

UUID的優點很明顯:“分佈式”、“唯一”。
這些優點使得UUID被廣泛使用,尤其是分佈式環境下。

然而其缺點也很明顯:無序,長度較長。
這些缺點也極大地限制了其應用範圍,比如數據表的主鍵,通常大家都不會用UUID。

但還是有不少地方用到UUID的:
有時候想給一個對象分配一個標識,但是該對象不好提取唯一特徵,然後該環境下又不好統一分配,
這時候很自然就想到UUID了,UUID不需要以對象特徵爲參數,也不用擔心重複(不是說不會重複,只是不用擔心,就像不用擔心天上掉下隕石砸到自己一樣-_-)。

壓縮

但是看着這個36個字節長度的UUID,總不自覺地會想有沒有優化的餘地。
16字節的信息,用16進制顯示,有32個字符,加上分隔符,有36字節。
事實上,如果用base64編碼這16個字節,可以壓縮到22字節。

    public static byte[] hex2Bytes(String hex) {
        if (hex == null || hex.isEmpty()) {
            return new byte[0];
        }
        byte[] bytes = hex.getBytes();
        int n = bytes.length >> 1;
        byte[] buf = new byte[n];
        for (int i = 0; i < n; i++) {
            int index = i << 1;
            buf[i] = (byte) ((byte2Int(bytes[index]) << 4) | byte2Int(bytes[index + 1]));
        }
        return buf;
    }

    private static int byte2Int(byte b) {
        return (b <= '9') ? b - '0' : b - 'a' + 10;
    }

    public static String compressUUID(String uuid){
        String hex = uuid.replace("-", "");
        byte[] bytes = FormatUtils.hex2Bytes(hex);
        return new String(Base64.encode(bytes, Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP));
    }

UUID壓縮前後:

d44979db-5c64-40f1-b47e-e7f41c4be9e7
3dkJ2-z92fr9DuD9rNvp4A

36字節相對22字節,節約接近40%的長度,對於存儲和傳輸而言,都是較大的提升;
雖然從可讀性來說,UUID的可讀性更好。
在權衡可讀性和性能的時候,筆者通常的想法是,如果閱讀和書寫比較頻繁,選擇可讀性較好的,如果不怎麼需要閱讀,選擇對機器友好的。
尤其是對於數據庫存儲這種情況,由於存在規模效應,顯然壓縮的版本更具性價比。

優化

如果需要壓縮版本的UUID,調用JDK的UUID生成字符串,再處理成壓縮版的UUID,顯然“繞圈子”了。
我們可以仿照JDK的寫法直接生成:

    public static String randomUUID() {
        byte[] bytes = new byte[15];
        Holder.numberGenerator.nextBytes(bytes);
        return Base64.encodeToString(bytes, Base64.URL_SAFE | Base64.NO_WRAP);
    }

15字節的隨機數,120bit, 和JDK的randomUUID效用上是差不多,然後15是3的倍數,base64編碼時不需要PADDING;
生成20字節的字符串(15 / 3 * 4), 相對UUID的36字節,節約近一半的空間。

其他

base64編碼有一個逼死強迫症的特點:除了常規字符[A-Za-z0-9]之外,需要另外兩個字符才能湊夠64個字符。
於是,我們看到base64分化了兩個版本,分別以 ['+', '/'] 和 ['-', '_'] 作爲補充字符的兩個版本。
其中,後者是URL_SAFE的版本,前者編碼後可能會包含'/', 而'/'是URL的分隔符。
但無論哪個版本,對於URL而言,有非常規字符確實確實不是很“美觀”。
於是,有人想出了base62編碼。
base62編碼,通常用來給long編碼還好,用來編碼任意字節數組的話,效率很低。
不過對於long來說,base62編碼長度爲11字節,而十六進制編碼也只是16個字節,而且十六進制可讀性更好。

簡書的文章ID,十六進制,12字節(48bit)。



12字節的長度,可讀性OK;48bit,取值範圍有兩百多萬億,夠用。總的來說,是比較均衡的方案。
我很好奇是怎麼構造的:
隨機數?可能性不大。
自增序列?不太像。通常純自增序列的ID長度不固定,如QQ號。

如果讓我來寫,有可能會混合多個因子來構造ID。
例如Twitter的Snowflake,混合了時間戳,機器ID和序列號。


計算機從16位寄存器,到32位,再到64位,就不往上漲了;
在當前的體系下,對於數據庫存儲而言,64bit的ID是最適合的。

總結

  • 儘量用整型的ID;
  • 如果要用UUID,儘量用壓縮的版本;
  • MD5也是128bit, 作爲字符串傳輸和存儲時,base64編碼要優於16進制。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章