閱讀目錄 單據號是指什麼 和唯一ID的不同是什麼 為什麼需要全局唯一單據號生成程式 實現的方式有哪些 筆者推薦的方式 結語 一、單據號是指什麼 我們作為一個軟體系統,肯定到處充滿著各種單據,也必然需要有各種單據號與之對應。比如:電商行業的訂單號、支付流水號、退款單號等等。SCM的採購單號、進貨單號、 ...
閱讀目錄
一、單據號是指什麼
我們作為一個軟體系統,肯定到處充滿著各種單據,也必然需要有各種單據號與之對應。比如:電商行業的訂單號、支付流水號、退款單號等等。SCM的採購單號、進貨單號、出貨單號、盤點單號等。在一個企業內部或者一個2C的平臺,無法避免的需要通過某個單據號來進行溝通。所以一個好的單據號必然是便於溝通的,簡單來說優先順序就是 好記 > 好輸入 > 好看,當然也是越短越好。
二、和唯一ID的不同是什麼
有的人可能會問,好像聽的最多的就是唯一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,...):
新的缺點:由於是多寫,所以需要依賴於負載均衡策略和網路通訊的延時問題,無法保證生成的序號是100%遞增的。(例:哪怕是round robin策略先請求1再請求2,但是還是有可能2先返迴響應。)
②垂直拆分多寫+自增列(機器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開始累加也不會產生重覆。
2)如果精度是毫秒,那麼相當於不管我的每秒併發量是多少,哪怕1秒就1個請求進來,也固定占用3位長度。但是如果是秒,那麼就省去了這3位,我想除了像阿裡騰訊這種體量的公司,實際的環境中毫秒併發達到1W已經不得了了。
其中還有一些細節是:
1.機器碼如果是個位數,那麼前面加0填充,以免與後面的自增列結合後產生重覆(例:機器1,序號11。和機器11,序號1會重覆)。
2.每個程式所在伺服器上的時鐘同步需要做好,因為我們依賴於此保證遞增問題。
最終,理論上實際生產環境生成的號碼長度在15~19之間。
六、結語
一個設計良好的單據號,不但可以用於主鍵,也可以用於做分庫分表,比如我們把用戶ID按照某個規則得出的幾位數字拼到單據號的最後,那麼直接用這個號來定位資料庫,可以確保一個用戶的訂單全部落在一個同一個資料庫里。
但是值得提醒的是,我們不能過於盲目的追求一步到位,需要結合自身的實際情況來選擇合適的方式就好。前面列出的一些常見的方案在系統初期也是能很好的工作的。
作者:Zachary_Fan
出處:http://www.cnblogs.com/Zachary-Fan/p/Global_Unique_No.html
如果你想及時得到個人自寫文章的消息推送,歡迎掃描下麵的二維碼~。