分布式系統中的必備良藥 全域性唯一單據號生成

2022-02-13 14:25:02 字數 4319 閱讀 2760

閱讀目錄

我們作為乙個軟體系統,肯定到處充滿著各種單據,也必然需要有各種單據號與之對應。比如:電商行業的訂單號、支付流水號、退款單號等等。scm的採購單號、進貨單號、出貨單號、盤點單號等。在乙個企業內部或者乙個2c的平台,無法避免的需要通過某個單據號來進行溝通。所以乙個好的單據號必然是便於溝通的,簡單來說優先順序就是 好記 > 好輸入 > 好看,當然也是越短越好。

有的人可能會問,好像聽的最多的就是唯一id,包括大量的文章都是講分布式唯一id的生成的,好像和單據號相關的很少。但是其實我覺得這2者並沒有衝突,只是重要性和針對場景不同。下面從不同的角度來分析一下:  

1)唯一性:唯一是id其實更多的是為了保證這個id在整個系統中都是唯一的,它對唯一的定義範圍更加廣。而對單據號來說,它只要保證在所屬的單據型別下唯一即可,比如訂單號:00001和物流號:00001其實並不相互影響。

2)可讀性:如果僅僅作為唯一id來用,其實最簡單粗暴的方式就是使用uuid,因為它僅僅給程式使用,人並不需要理解這個id的意義。但是單據號則不同,上面也提到了,它需要有一定的可讀性,便於人與人之間的溝通。想象一下你和其它人**溝通時報一串uuid是什麼體驗。

3)業務性:單據號大部分情況下還需要承擔一定的業務含義的體現,比如訂單號t00001中的t = trade、支付號p00001中的p = pay等。甚至還有可能需要多筆單據號之間有一定的關聯,比如乙個訂單號t00001下相關的支付號都必須是p00001-1,p00001-2這個樣子。再甚至有些場景需要包含一些日期資訊在其中。

和唯一id一樣,單據號的生成本身也是乙個相對穩定並且通用的規則,所以把它提煉成乙個單獨的程式可以提供更好的復用性,避免了各自專案維護單據號所花費的重複勞動。特別在網際網路行業中的大流量企業,還需要考慮效能和高可用問題。所以真的要把生成單據號這個「小功能」做好,還是需要一定的投入的。那麼把它作為乙個單獨的程式能夠把投入所產生的收益,也就是所謂的「roi」放大,何樂而不為?

下面羅列一下常用的實現方式和各自的優缺點:

1)字首列+全域性自增列:

這個和唯一id的方案類似,利用自增列的數字來做。且最簡單的方式就是依賴資料庫的自增列來做。

優點:

實現簡單,不斷的++

能夠保證全域性的唯一性

能夠保證遞增

可讀性尚可

缺點:

需要依賴乙個持久化的地方儲存當前已經生成的「游標」位置,所以效能有上限,基本就是單應用的tps上限或者所依賴db的tps上限

在一些對外的單據號上容易洩露一些商業資訊。比如競爭對手可以通過單號猜出你每天的訂單量甚至每個小時、每分鐘的訂單量。

破除單點的改進方案:

①水平拆分進行多寫+同步長(例:機器1的自增數為1,4,7,...;機器2的自增數為2,5,8,...;機器3的自增數為3,6,9,...):

②垂直拆分多寫+自增列(機器1專門用於生成訂單號、機器2專門用於生成支付單號):

新的缺點:

a.由於根據業務來分,所以流量不均導致某些大請求量的單據還是存在著單點瓶頸問題。

b.擴充套件性較差。每增加乙個業務單據就需要增加乙個程式

③水平拆分+增加機器碼位(給每台生成單據號的程式編個號:1,2,3插入到自增列的前面):

新的缺點:

a.這個編碼要麼硬配置到配置檔案中,或者依賴與某個分配編號的獨立程式。並且號碼長度變長了。

b.無法保證遞增。

提高效能的改進方案:

①預生成到快取,減少對db的依賴

新的缺點:

a.如果需要徹底減少對db的依賴,那麼每次單據號被消耗是不應該回寫db的,也導致了一旦程式重啟會存在比較大的序號空洞。

b.快取的大小與db獲取下一段快取資料的頻率負相關的,當頻率比較高的時候,需要做雙快取來預載入下一段快取資料,避免快取消耗完之後從db拉取最新資料產生的阻塞。

2)字首列+日期+自增列:

我想這個方案應該是大部分系統會採用的方案。這個日期的精度和自增數的資料長度是有關聯的。日期精度越高,對於自增數的資料長度需求就越短,反之則越長。

優點:

實現比較容易

能夠保證唯一性

能夠保證遞增

包含日期能體現更多的業務資訊

缺點:

方案1的缺點都有

針對日期讓自增列進行重置需要做一定的邏輯判斷,複雜度提高(在多執行緒下有執行緒安全問題),效能降低。

破除單點的改進方案:

① 1)中的改進方案。

提高效能的改進方案

① 1)中的改進方案。

② 對自增列的重置可以忽略日期變動(也就是哪怕到了下乙個時間段,自增數也不重置,繼續使用),而直接對整數進行++,直到自動進入下一迴圈。在c#中,你可以這樣寫:

var uint32 = (long

)uint32.maxvalue;

interlocked.add(

ref uint32, 1

); console.writeline((uint32)uint32);

但是這裡需要注意的是,這個自增列的數字上限必須能保證在日期的最小精度範圍內不會產生重複。

新的缺點:

a.哪怕請求量不大,也會產生過長的單據號,因為自增數不會主動重置。

筆者個人覺得綜合來看,

增加機器碼位(給每台生成單據號的程式編個號:1,2,3插入到自增列的前面)

這個方案是相對最一勞永逸的。但是需要在資料長度和可讀性上需要做出一定的權衡。首先為了保證遞增,那麼我們必然需要增加時間到整個單據號的前面。時間可以使用常規的日期格式也可以使用時間戳,當然相同精度來說,肯定是時間戳更短。考慮到實際的大部分場景中,單據號只要能夠識別到是哪一種型別的單據,剩下的一般來說本身就是需要去對應的單據列表中找到該筆單據的詳細資訊檢視。所以其實對日期的可讀性並不是那麼高。(舉個例子:客戶報出乙個訂單號出來給我們的客服人員,其實客服人員必然是需要去檢視這筆訂單的詳細資訊的。)

ok,那它的長度我們可以如此來設計:

其中時間戳、自增數是全域性共用的,所以對於單獨某一型別的單據號並不是連續的,但是是趨勢遞增的,這解決了根據訂單號猜到訂單量之類的問題。

那麼在這樣的設計下可以支撐單據號不重複的上限是多少呢?其實就是單點在1秒內的最大量100000000 /1000 = 100000/ms,1毫秒10w個,以snowflake的生成速度4000/ms來算(網路**,未經實際驗證),再根據摩爾定律考慮cpu公升級的影響,大約需要50年後才有可能產生重複。並且理論最大值是100臺程式負載均衡,1000w/ms,估計這輩子不用考慮重複問題了。

有的人可能會問,為什麼不直接時間戳取到毫秒位,會增加3位長度,後面自增數就可以短一點。首先按照比snowflake演算法多冗餘乙個位數來看,哪怕取到時間戳到毫秒,後面還是需要5位(snowflake是4位:4000/ms),所以這個並沒有什麼區別。那麼精度取到秒的好處是什麼?我認為有2點:

1)解決了預載入問題,由於精度到秒,所以哪怕程式重啟了,我的自增數從0開始累加也不會產生重複。

其中還有一些細節是:

1.機器碼如果是個位數,那麼前面加0填充,以免與後面的自增列結合後產生重複(例:機器1,序號11。和機器11,序號1會重複)。

2.每個程式所在伺服器上的時鐘同步需要做好,因為我們依賴於此保證遞增問題。

最終,理論上實際生產環境生成的號碼長度在15~19之間。

乙個設計良好的單據號,不但可以用於主鍵,也可以用於做分庫分表,比如我們把使用者id按照某個規則得出的幾位數字拼到單據號的最後,那麼直接用這個號來定位資料庫,可以確保乙個使用者的訂單全部落在乙個同乙個資料庫裡。

但是值得提醒的是,我們不能過於盲目的追求一步到位,需要結合自身的實際情況來選擇合適的方式就好。前面列出的一些常見的方案在系統初期也是能很好的工作的。

出處:的***~。

定期發表原創內容:架構設計丨分布式系統丨產品丨運營丨一些思考。

」,回覆「運營」,送你乙份我長期收集和整理的思維導圖。

分布式系統中的必備良藥 服務治理

閱讀目錄 在分布式系統的構建之中,服務治理是類似血液一樣的存在,乙個好的服務治理平台可以大大降低協作開發的成本和整體的版本迭代效率。在服務治理之前,簡單粗暴的rpc呼叫使用的點對點方式,完全通過人為進行配置操作決定,運維成本高 每次布置1套新的環境需要改一堆配置檔案的ip 還容易出錯,且整個系統執行...

分布式系統全域性唯一ID

全域性的唯一流水id 可以將乙個請求在分布式系統中的流轉路徑聚合。生成唯一id有兩種方法 持久型 使用資料庫表自增欄位或者sequence 生成,為了提高效率,每個應用節點可以快取乙個批次的id 如果機器重啟則可能會損失一部分id 但是這並不會產生任何問題。時間型 一般由機器號 業務號 時間 單節點...

分布式系統全域性唯一ID生成

在複雜分布式系統中,往往需要對大量的資料和訊息進行唯一標識。如在金融 電商 支付 等產品的系統中,資料日漸增長,對資料分庫分表後需要有乙個唯一id來標識一條資料或訊息,資料庫的自增id顯然不能滿足需求,此時乙個能夠生成全域性唯一id的系統是非常必要的。同時除了對id號碼自身的要求,業務還對id號生成...