最近銀聯一紙 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,格式如下:
這裡就不解釋 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
可以看到這個數字已經很大,未來很長一段時間都不可能超過(除非後續開展銀地球外的服務)。
那怎麼生成一個帶有英文字母的序號呢?難道需要重寫一個發號器嗎?
其實我轉換一下思路,裝置號規則可以使用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 的應用。
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 的缺點,不容易記憶。
B 站以前還是 AV 號的時候,採用的是 Base10 ,這個非常容易記憶。
比如 B 站鎮站之寶:av170001
以前我們可以通過彈幕直接傳送 AV 號,感興趣就可以直接輸入跳轉到那條視訊。
但是現在 10 位 BV 號,幾乎難以記憶,在彈幕中輸入 BV 號都很困難。
哎,少了當初的快樂的。