分佈式 ID 生成服務的技術原理和項目實戰
作者 | 文庫 App
導讀
ID 在我們的開發工作和日常生活中使用的非常頻繁,幾乎只要是在開發就會天天打交道,它的應用場景十分廣泛,比如:身份證號,下單生成的訂單號,購買的聯合會員商品的兌換券碼。不同場景對 ID 生成服務的要求不同,以下我們逐個分析。
01 什麼是分佈式 ID 生成服務
在業務開發中,大量場景需要唯一 ID 來進行標識:用戶獨一無二的身份認證、超市售賣的商品、微信的即時消息,它們都需要標識來確定唯一性。需要在特定範圍內保證 ID 具備唯一性,這是 ID 生成服務最基本的要求。
生成 ID 的方式多種多樣,可以使用 Redis 鍵自增,UUID,或者基於雪花算法實現的 ID 生成服務。最常見的基於數據庫 ID 自增的方式,在業務數據量不大的時候,單庫單表可以支撐,數據再大一點搞個 MySQL 主從同步、讀寫分離也能對付。但隨着數據日漸增長,主從同步也扛不住了,就需要對數據庫進行分庫分表,但分庫分表後需要有一個唯一 ID 來標識一條數據,數據庫的自增 ID 顯然不能滿足需求。
伴隨着業務快速迭代,很多業務都需要生成 ID,各自爲政會陷入 "重複造輪子" 的低效勞動中,同時造成服務治理上的混亂,此時一個能夠生成全局唯一 ID 的服務是非常必要的。那麼這個全局唯一 ID 就叫分佈式 ID 生成服務。
02 服務特性
① 唯一性:生成的 ID 唯一,特定範圍不衝突;
② 有序性:生成的 ID 按某種規則有序,趨勢遞增,便於入庫和查詢,但不嚴格要求;
③ 高可用、高性能:高併發下的具備高可用,確保任何情況能容災,穩定提供服務;
④ 自主性:分佈式環境下不依賴中心認證即可自行生成 ID;
⑤ 安全性:脫敏,不暴露系統和業務的信息,如:訂單數,用戶數。
03 常見的技術實現方式
04 技術爲業務服務
技術歸根到底是爲業務服務,要在業務中體現技術的價值。
網上絕大多數的分佈式 id 生成服務,一般着重於技術原理剖析,很少見到根據具體的業務場景去選型 ID 生成服務的文章。
本文結合一些使用場景,進一步探討業務場景中對 ID 有哪些具體的要求。
4.1 場景一:訂單系統
我們在商場買東西一碼付二維碼,下單生成的訂單號,使用到的優惠券碼,聯合商品兌換券碼,這些是在網上購物經常使用到的單號,那麼爲什麼有些單號那麼長,有些只有幾位數?有些單號一看就知道年月日的信息,有些卻看不出任何意義?下面展開分析下訂單系統中不同場景的 id 服務的具體實現。
1、一碼付
我們常見的一碼付,指的是一個二維碼可以使用支付寶或者微信進行掃碼支付。
二維碼的本質是一個字符串。聚合碼的本質就是一個鏈接地址。用戶使用支付寶微信直接掃一個碼付錢,不用擔心拿支付寶掃了微信的收款碼或者用微信掃了支付寶的收款碼,這極大減少了用戶掃碼支付的時間。
實現原理是當客戶用 APP 掃碼後,網站後臺就會判斷客戶的掃碼環境。(微信、支付寶、QQ 錢包、京東支付、雲閃付等)。
判斷掃碼環境的原理就是根據打開鏈接瀏覽器的 HTTP header。任何瀏覽器打開 http 鏈接時,請求的 header 都會有 User-Agent(UA、用戶代理) 信息。
UA 是一個特殊字符串頭,服務器依次可以識別出客戶使用的操作系統及版本、CPU 類型、瀏覽器及版本、瀏覽器渲染引擎、瀏覽器語言、瀏覽器插件等很多信息。
各渠道對應支付產品的名稱不一樣,一定要仔細看各支付產品的 API 介紹。
-
微信支付:JSAPI 支付支付
-
支付寶:手機網站支付
-
QQ 錢包:公衆號支付
其本質均爲在 APP 內置瀏覽器中實現 HTML5 支付。
文庫的研發同學在這個思路上,做了優化迭代。動態生成一碼付的二維碼預先綁定用戶所選的商品信息和價格,根據用戶所選的商品動態更新。這樣不僅支持一碼多平臺調起支付,而且不用用戶選擇商品輸入金額,即可完成訂單支付的功能,很絲滑。用戶在真正掃碼後,服務端才通過前端獲取用戶 UID,結合二維碼綁定的商品信息,真正的生成訂單,發送支付信息到第三方(qq、微信、支付寶),第三方生成支付訂單推給用戶設備,從而調起支付。
區別於固定的一碼付,在文庫的應用中,使用到了動態二維碼,二維碼本質是一個短網址,ID 服務提供短網址的唯一標誌參數。唯一的短網址映射的 ID 綁定了商品的訂單信息,技術和業務的深度結合,縮短了支付流程,提升用戶的支付體驗。
2、訂單號
訂單號在實際的業務過程中作爲一個訂單的唯一標識碼存在,一般實現以下業務場景:
-
用戶訂單遇到問題,需要找客服進行協助;
-
對訂單進行操作,如線下收款,訂單核銷;
-
下單,改單,成單,退單,售後等系統內部的訂單流程處理和跟進。
很多時候搜索訂單相關信息的時候都是以訂單 ID 作爲唯一標識符,這是由於訂單號的生成規則的唯一性決定的。從技術角度看,除了 ID 服務必要的特性之外,在訂單號的設計上需要體現幾個特性:
(1)信息安全
編號不能透露公司的運營情況,比如日銷、公司流水號等信息,以及商業信息和用戶手機號,身份證等隱私信息。並且不能有明顯的整體規律(可以有局部規律),任意修改一個字符就能查詢到另一個訂單信息,這也是不允許的。
類比於我們高考時候的考生編號的生成規則,一定不能是連號的,否則只需要根據順序往下查詢就能搜索到別的考生的成績,這是絕對不可允許。
(2)部分可讀
位數要便於操作,因此要求訂單號的位數適中,且局部有規律。這樣可以方便在訂單異常,或者退貨時客服查詢。
過長的訂單號或易讀性差的訂單號會導致客服輸入困難且易錯率較高,影響用戶體驗的售後體驗。因此在實際的業務場景中,訂單號的設計通常都會適當攜帶一些允許公開的對使用場景有幫助的信息,如時間,星期,類型等等,這個主要根據所涉及的編號對應的使用場景來。
而且像時間、星期這些自增長的屬於作爲訂單號的設計的一部分元素,有助於解決業務累積而導致的訂單號重複的問題。
(3)查詢效率
常見的電商平臺訂單號大多是純數字組成,兼具可讀性的同時,int 類型相對 varchar 類型的查詢效率更高,對在線業務更加友好。
3、優惠券和兌換券
優惠券、兌換券是運營推廣最常用的促銷工具之一,合理使用它們,可以讓買家得到實惠,商家提升商品銷量。常見場景有:
-
在文庫購買【文庫 VIP+QQ 音樂年卡】聯合商品,支付成功後會得到 QQ 音樂年卡的兌換碼,可以去 QQ 音樂 App 兌換音樂會員年卡;
-
疫情期間,部分地方政府發放的消費券;
-
瓶裝飲料經常會出現輸入優惠編碼兌換獎品。
從技術角度看,有些場景適合 ID 即時生成,比如電商平臺購物領取的優惠券,只需要在用戶領取時分配優惠券信息即可。有些線上線下結合的場景,比如疫情優惠券,瓶蓋開獎,京東卡,超市卡這種,則需要預先生成,預先生成的券碼具備以下特性:
-
預先生成,在活動正式開始前提供出來進行活動預熱;
-
優惠券體量大,以萬爲單位,通常在 10 萬級別以上;
-
不可破解、仿製券碼;
-
支持用後覈銷;
-
優惠券、兌換券屬於廣撒網的策略,所以利用率低,也就不適合使用數據。
庫進行存儲(佔空間,有效的數據有少)
設計思路上,需要設計一種有效的兌換碼生成策略,支持預先生成,支持校驗,內容簡潔,生成的兌換碼都具有唯一性,那麼這種策略就是一種特殊的編解碼策略,按照約定的編解碼規則支撐上述需求。
既然是一種編解碼規則,那麼需要約定編碼空間 (也就是用戶看到的組成兌換碼的字符),編碼空間由字符 a-z,A-Z, 數字 0-9 組成,爲了增強兌換碼的可識別度,剔除大寫字母 O 以及 I, 可用字符如下所示,共 60 個字符:
abcdefghijklmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXZY0123456789
之前說過,兌換碼要求近可能簡潔,那麼設計時就需要考慮兌換碼的字符數,假設上限爲 12 位,而字符空間有 60 位,那麼可以表示的空間範圍爲 60^12=130606940160000000000000(也就是可以 12 位的兌換碼可以生成天量, 應該夠運營同學揮霍了),轉換成 2 進制:1001000100000000101110011001101101110011000000000000000000000(61 位)
兌換碼組成成分分析
兌換碼可以預先生成,並且不需要額外的存儲空間保存這些信息,每一個優惠方案都有獨立的一組兌換碼 (指運營同學組織的每一場運營活動都有不同的兌換碼, 不能混合使用, 例如雙 11 兌換碼不能使用在雙 12 活動上),每個兌換碼有自己的編號,防止重複,爲了保證兌換碼的有效性,對兌換碼的數據需要進行校驗,當前兌換碼的數據組成如下所示:
優惠方案 ID + 兌換碼序列號 i + 校驗碼
編碼方案
-
兌換碼序列號 i,代表當前兌換碼是當前活動中第 i 個兌換碼,兌換碼序列號的空間範圍決定了優惠活動可以發行的兌換碼數目,當前採用 30 位 bit 位表示,可表示範圍:1073741824(10 億個券碼)。
-
優惠方案 ID, 代表當前優惠方案的 ID 號,優惠方案的空間範圍決定了可以組織的優惠活動次數,當前採用 15 位表示,可以表示範圍:32768(考慮到運營活動的頻率,以及 ID 的初始值 10000,15 位足夠,365 天每天有運營活動,可以使用 54 年)。
-
校驗碼,校驗兌換碼是否有效,主要爲了快捷的校驗兌換碼信息的是否正確,其次可以起到填充數據的目的,增強數據的散列性,使用 13 位表示校驗位,其中分爲兩部分,前 6 位和後 7 位。
深耕業務還會有區分通用券和單獨券的情況,分別具備以下特點,技術實現需要因地制宜地思考。
-
通用券:多個玩家都可以輸入兌換,然後有總量限制,期限限制。
-
單獨券:運營同學可以在後臺設置兌換碼的獎勵物品、期限、個數,然後由後臺生成兌換碼的列表,兌換之後覈銷。
4.2 Tracing
1、日誌跟蹤
在分佈式服務架構下,一個 Web 請求從網關流入,有可能會調用多個服務對請求進行處理,拿到最終結果。這個過程中每個服務之間的通信又是單獨的網絡請求,無論請求經過的哪個服務出了故障或者處理過慢都會對前端造成影響。
處理一個 Web 請求要調用的多個服務,爲了能更方便的查詢哪個環節的服務出現了問題,現在常用的解決方案是爲整個系統引入分佈式鏈路跟蹤。
在整個請求的調用鏈中,請求會一直攜帶 traceid 往下游服務傳遞,每個服務內部也會生成自己的 spanid 用於生成自己的內部調用視圖,並和 traceid 一起傳遞給下游服務。
2、TraceId 生成規則
這種場景下,生成的 ID 除了要求唯一之外,還要求生成的效率高、吞吐量大。traceid 需要具備接入層的服務器實例自主生成的能力,如果每個 trace 中的 ID 都需要請求公共的 ID 服務生成,純純的浪費網絡帶寬資源。且會阻塞用戶請求向下遊傳遞,響應耗時上升,增加了沒必要的風險。所以需要服務器實例最好可以自行計算 tracid,spanid,避免依賴外部服務。
產生規則:服務器 IP + ID 產生的時間 + 自增序列 + 當前進程號 ,比如:
0ad1348f1403169275002100356696
前 8 位 0ad1348f 即產生 TraceId 的機器的 IP,這是一個十六進制的數字,每兩位代表 IP 中的一段,我們把這個數字,按每兩位轉成 10 進制即可得到常見的 IP 地址表示方式 10.209.52.143,您也可以根據這個規律來查找到請求經過的第一個服務器。
後面的 13 位 1403169275002 是產生 TraceId 的時間。之後的 4 位 1003 是一個自增的序列,從 1000 漲到 9000,到達 9000 後回到 1000 再開始往上漲。最後的 5 位 56696 是當前的進程 ID,爲了防止單機多進程出現 TraceId 衝突的情況,所以在 TraceId 末尾添加了當前的進程 ID。
3、SpanId 生成規則
span 是層的意思,比如在第一個實例算是第一層, 請求代理或者分流到下一個實例處理,就是第二層,以此類推。通過層,SpanId 代表本次調用在整個調用鏈路樹中的位置。
假設一個 服務器實例 A 接收了一次用戶請求,代表是整個調用的根節點,那麼 A 層處理這次請求產生的非服務調用日誌記錄 spanid 的值都是 0,A 層需要通過 RPC 依次調用 B、C、D 三個服務器實例,那麼在 A 的日誌中,SpanId 分別是 0.1,0.2 和 0.3,在 B、C、D 中,SpanId 也分別是 0.1,0.2 和 0.3;如果 C 系統在處理請求的時候又調用了 E,F 兩個服務器實例,那麼 C 系統中對應的 spanid 是 0.2.1 和 0.2.2,E、F 兩個系統對應的日誌也是 0.2.1 和 0.2.2。
根據上面的描述可以知道,如果把一次調用中所有的 SpanId 收集起來,可以組成一棵完整的鏈路樹。
spanid 的生成本質:在跨層傳遞透傳的同時,控制大小版本號的自增來實現的。
4.3 場景三:短網址
短網址主要功能包括網址縮短與還原兩大功能。相對於長網址,短網址可以更方便地在電子郵件,社交網絡,微博和手機上傳播,例如原來很長的網址通過短網址服務即可生成相應的短網址,避免折行或超出字符限制。
常用的 ID 生成服務比如:mysql ID 自增、 redis 鍵自增、號段模式,生成的 ID 都是一串數字。短網址服務把客戶的長網址轉換成短網址,
實際是在 dwz.cn 域名後面拼接新產生的數字類型 ID,直接用數字 ID,網址長度也有些長,服務可以通過數字 ID 轉更高進制的方式壓縮長度。這種算法在短網址的技術實現上越來越多了起來,它可以進一步壓縮網址長度。轉進制的壓縮算法在生活中有廣泛的應用場景,舉例:
客戶的長網址:https://wenku.baidu.com/ndbusiness/browse/wenkuvipcashier?cashier_code=PCoperatebanner
ID 映射的短網址:https://dwz.cn/2047601319t66
轉進制後的短網址:https://dwz.cn/2ezwDJ0
長數字轉短字符串的壓縮算法,以下爲具體實現:
/**
* 10進制轉爲62進制
*
* @param integer $n 10進制數值
* @return string 62進制
*/
function dec62($n) {
$base = 62;
$index = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$ret = '';
for($t = floor(log10($n) / log10($base)); $t >= 0; $t --) {
$a = floor($n / pow($base, $t));
$ret .= substr($index, $a, 1);
$n -= $a * pow($base, $t);
}
return $ret;
}
端字符串同樣支持反解,將高位進制轉爲 10 進制。
/**
* 62進制轉爲10進制
*
* @param integer $n 62進制
* @return string 10進制
*/
function dec10($s) {
$base = 62;
$index = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$ret = 0;
$len = strlen($s) - 1;
for($t = 0; $t <= $len; $t ++) {
$ret += strpos($index, substr($s, $t, 1)) * pow($base, $len - $t);
}
return $ret;
}
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ 依次表示 0-62,大小寫表示不同的進制。10 進制的數字採取 62 進位壓縮的方式轉爲字符串,僅需要 6 位就可以滿足 500 多億的網址(568 0023 5583),唯一性得到滿足的同時保證長度夠短,在具體實現中也可隨機跳躍生成,防禦撞庫攻擊。
05 總結
在諸多需要 ID 服務的業務中,聯合商品需要做好信息加密,兌換碼、優惠券需要支持可反解或者可驗證,訂單號需要支持部分信息可讀,可以輕鬆獲取日期信息。
純數字組成的用戶 uid,訂單號出於數據安全,要求是非連續的,但是如果是一些需要計數的 ID,則要求嚴格遞增。短 url 服務中的 ID 對長度有很高要求,traceid 則要求 ID 可反解,且支持本地生成,保證局部唯一就行。
由此可見,ID 生成服務除了具備基礎特性,部分場景下還需要滿足的特定需求。制定合適的技術方案結合場景去實現,發揮技術優勢,纔可以更好的支撐業務發展。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/bFDLb6U6EgI-DvCdLTq_QA