巧用 Base62 解決欄位太短的問題

樓下小黑哥 發表於 2022-01-23

最近銀聯一紙 259 號改造通知,所有支付機構開始改造支付交易,上傳終端資訊。

不知道其他支付機構的小夥伴針對這次改造是否開始了?

由於這次銀聯給的時間非常少,我們這邊改動涉及到相關上游一起改造,所以我們已經開始設計,馬上投入程式碼改造中。

這次改造,支付機構內部需要維護交易發生的終端資訊。在銀聯的改造文件中,支付機構內部需要為每一臺終端分配一個唯一的終端裝置號。

銀聯側規定這個欄位是 String(8),可以使用英文字母以及數字,即 a-z/A-Z/0-9。

由於我們內部發號器,只能產生純數字的序號,按照 8 位長度,從 0 開始最多隻能有 99,999,999 。

當前看來沒什麼問題,但是千萬級數字還是太小,一不小心就沒了。為了後續系統的穩定性,所以我們需要設計一種滿足銀聯的規則終端號生成方法。

發號器現狀

先來介紹先,我們這邊的發號器現狀,應該跟很多公司做法都比較類似。

我們這邊發號器,有兩種生成策略:

  • simple
  • snowflake

第一種策略simple 型別,這種策略方式非常簡單,類似 MySQL ID 自增策略,可以指定從某個數字開始遞增。

這種方適用於表ID 等場景,只需要保證唯一,不需要保證順序。

第二種策略 snowflake 型別,即使用雪花演算法。

這種方式適用於訂單ID 等需要保留時間資訊的場景。

這兩種發號器都存在一定的問題,沒辦法直接適用於銀聯終端號的場景。

simple 型別發號器問題

這個型別發號器只能發純數字的序號,按照 8 位長度,從 0 開始最多隻能有 99,999,999 。

由於發號器雖然是單調遞增的,但是可能存在機房配置,存在跳號的情況,這就導致可能可以用的序號少於 99,999,999 。

所以不能拿來直接使用。

snowflake 型別發號器的問題

snowflake 型別發號器的發出來的序號是 64 bit,格式如下:

巧用 Base62 解決欄位太短的問題

這裡就不解釋 snowflake 策略具體的原理,舉一個 snowflake生成的序號:

170916032679263329

可以看到 snowflake 發號器生成這個 64 bit 整數非常大,位數也遠遠大於 8 位。

裝置號生成方法

上面說到裝置號生成規則是允許英文字母,即 a-z/A-Z/0-9。

如果我們僅僅使用數字,那麼我們僅僅只有 10^8-1= 99,999,999

那我們轉換一種思路,我們把英文字母也用,那麼我們總共可以有62^8-1=218,340,105,584,895

可以看到這個數字已經很大,未來很長一段時間都不可能超過(除非後續開展銀地球外的服務)。

那怎麼生成一個帶有英文字母的序號呢?難道需要重寫一個發號器嗎?

巧用 Base62 解決欄位太短的問題

其實我轉換一下思路,裝置號規則可以使用a-z/A-Z/0-9,裝置號每一位都有 62 個選擇,那站在數學的角度,這不就是 62 進位制的嗎?

那現在我們使用發號器生成的序號只能是整數,那站在數學角度,是一個十進位制的數。

那我們只要把這個10 進位制數轉成 62 進位制數,這不就可以解決問題了嗎!

所以這裡我們使用 simple 型別的發號器,從 0 開始遞增。

拿到發號器生成的序號之後,我們只需要將其轉為 62 進位制就可以了。

10 進位制轉 62 二進位制的程式碼示例如下:

public class TermInfoUtils {
    /**
     * https://en.wikipedia.org/wiki/Base62#/
     * wiki 上的標準
     */
    private final static char[] charArray = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".toCharArray();

    private static final int BASE_62_SCALE = 62;

    /**
     * base10 轉 base 62
     *
     * @param base10
     * @return
     */
    public static String fromBase10ToBase62(long base10) {
        int scale = 62;
        StringBuilder sb = new StringBuilder();
        long remainder = 0;

        do {
            remainder = base10 % scale;
            sb.append(charArray[(int) remainder]);
            base10 = base10 / scale;
        }
        while (base10 > scale - 1);

        sb.append(charArray[(int) base10]);

        // 倒序
        return sb.reverse().toString();
    }

    /**
     * 產生銀聯終端號
     * @param base10
     * @return
     */
    public static String generateUnionDeviceId(long base10) {
        String base62 = fromBase10ToBase62(base10);
        // 往左補 0
       return StringUtils.leftPad(base62,8,"0");
    }
        
}

程式碼原理非常簡單,類似 10 進位制轉二進位制,除二取餘,然後倒序排列,高位補零。轉62進位制也類似,不斷除以62取餘數,然後倒序。

最後,為了我們生成的序號長度統一,我們定一個規則,如果生成的 62 進位制小於 8 位,那左邊補 0 ,直到整個長度為 8 位。

## Base62 其他妙用

除了上面這個應用之外,其實現實也有很多應用也是使用 Base62 解決。

比如,我們現在常用的短網址服務,隨便生成一個短網址。

https://tinyurl.com/y8jvg3eb

我們可以看到短網址最後都有一串字串,那其實就是 Base62。

ps:有些短網址採用的是 Base64,即 a-z/A-Z/0-9/_- ,但是原理類似。

再比如,B 站現在使用的 BV 號,其實也是 Base62 的應用。

巧用 Base62 解決欄位太短的問題

ps:按照網上說法,BV 號實際上是 Base58,去除用數字"0",字母大寫"O",字母大寫"I",和字母小寫"l",以及"+"和"/"符號。

那實際上,上面這兩種方式都是將 Base62 當做一個唯一 ID。這種方式有一個非常明顯的優點,可以用一個只有幾位長度字串,容納超多超多的 ID。

我們對比一下,通常使用純數字形式 ID,即 Base10與 Base62 。

當 ID 長度為 4 位的時候,Base10 可以容納的數字:

$$
10^4-1=9999
$$

而 Base62 可以容納

$$
62^4-1=14776335
$$

而當 ID 長度到達 5 位的時候,Base10 可以容納:

$$
10^5-1=99999
$$

而 Base 62 可以有:

$$
62^5-1=916132831
$$

通過對比我們可以發現,Base62 可以容納數字遠遠超過 Base10,並且根本不是一個量級的。

另外假設 5 位長度 Base62 用完了,只要加上一位,又可以容納 62 倍數字,幾乎很難再用完。

說完優點,我們再來聊聊 Base62 的缺點,不容易記憶。

巧用 Base62 解決欄位太短的問題

B 站以前還是 AV 號的時候,採用的是 Base10 ,這個非常容易記憶。

比如 B 站鎮站之寶:av170001

巧用 Base62 解決欄位太短的問題

以前我們可以通過彈幕直接傳送 AV 號,感興趣就可以直接輸入跳轉到那條視訊。

但是現在 10 位 BV 號,幾乎難以記憶,在彈幕中輸入 BV 號都很困難。

哎,少了當初的快樂的。