如何設計一個訂單號生成服務?應該考慮那些問題?
description: 在高并發的電商系統中,生成全局唯一的訂單編號是關鍵。本文探討了幾種常見的訂單編號生成方法,包括UUID、數據庫自增、雪花算法和基于Redis的分布式組件,并詳細分析了它們的優缺點。讓你在面試過程中更進一步。
這邊先來看一個場景:
在實際開發過程中,業務中最常見的一個服務就是生成業務單號,比如電商訂單編號、入庫單號、服務單號等等。那么如果讓你創建一個訂單號,你會考慮那些問題?有什么好的設計思路嘛?
需求分析
考慮一下如果讓你設計一個服務單號,需要滿足那些基本需求呢?、
- 全局唯一性: 訂單號需要在在整個系統獨一無二,避免重復
- 安全性: 訂單號不能暴漏太多信息,比如流水信息、用戶信息,那么自增這種方案就不能考慮了
- 禁用隨機碼: 隨機碼雖然可以滿足前兩個條件,但是隨機碼沒有更多信息,比如相對順序、日期信息,同時伴有一定概率會重復
- 滿足并發需求: 像在特定場合下(如秒殺)需要做到并發場景下的訂單號的生成
- 控制位數: 訂單號的位數盡量在 10 位 ~ 18 位之間。太短的情況下,如果交易量過大,很難做到防止重復,太長可讀性差、意義也不大。
- 有業務含義: 訂單號盡可能包含業務信息,比如訂單時間、業務類型等
再此基礎上如何設計一個優秀的訂單號服務呢?這個時候最好梳理一下功能點,需要滿足那些要求:
設計目標:
- 滿足高并發需求
- 可拓展性高
- 滿足峰值壓力
- 支持分布式架構
- 檢索效率,如果訂單號存儲在數據庫中,檢索效率需要考慮
設計方案
接下來會闡述一些常見的方案,再談論一下這種解決方案的優缺點。
方案一:數據庫自增ID
所謂數據庫自增,意思是在數據庫中給某個列設置為自增列,并且給該列設置一個初始值,代碼層面無需任何特殊處理,以 Mysql 的用戶表 ID 列為例,可以通過如下方式在創建表的時候生產。
CREATE TABLE `tb_user` (`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,`name` varchar(20) DEFAULT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
優點:
- 實現簡單
- 保證唯一性
缺點:
- 數據庫存在性能瓶頸,特別是高并發情況下
- 分布式數據庫,在多個實例情況下難以保證全局唯一、
- 安全性低,自增ID容易暴露信息
- 無業務含義
適用場景:
如果是單體服務下,并且是初始業務情況下可以使用,但需要對數據庫進行分庫分表會出現重復ID。不建議直接使用這種方式,上限較低。
方案二:UUID
UUID 是Universally Unique Indentifier
的縮寫,翻譯為通用唯一識別碼,顧名思義 UUID 是一個用于記錄唯一標識一條的數據,其按照開放軟件基金會(OSF)指定的標準進行計算,用到了以太網卡地址(MAC)、納秒級時間、芯片 ID 碼和許多可能的數字。
總的來說,UUID 碼由以下三部分組成:
- 當前日期和時間
- 時鐘序列
- 全局唯一的 IEEE 機器識別碼(如果有網卡從網卡獲得,沒有網卡則通過其他方式獲得)
UUID 的標準形式包含 32 個 16 進制數字,以連字號分為五段,示例:00000191-adc6-4314-8799-5c3d737aa7de
。
以java
為例,通過以下方式即可生成:
String uuid = UUID.randomUUID().toString();
優點:
- 全局唯一性
- 簡單易用
- 安全性: UUID的隨機性使得它不容易被猜測或預測,增加了數據的安全性
- 無需集中管理:由于UUID是本地生成的,不需要依賴于中心化的ID生成服務,減少了單點故障的風險。
- 可擴展性:UUID的生成過程是分布式的,可以在多個節點上并行生成,適合大規模分布式系統。
缺點:
- 長度較長
- 可讀性差: 無業務含義,難以理解
- 索引效率低: UUID的隨機性會導致插入時的索引分裂和碎片化,從而降低寫入性能,查詢效率也低
- 排序問題: UUID不具備自然的時間順序,因此不適合用于需要按時間順序進行排序的場景
適用場景:
如果是在分布式系統中,對訂單號有著特別高的要求,并且不需要長期持久化存儲以及不需要頻繁查詢那么就可以推薦使用。
分布式文件存儲系統,該系統需要處理大量的文件上傳、下載和管理操作。這些文件可能來自不同的用戶和設備,并且需要在多個服務器之間進行分布存儲和訪問。為了確保每個文件都有一個唯一的標識符,并且能夠在全球范圍內保持唯一性,UUID是一個非常適合的選擇。
方案三: 雪花算法
Snowflake(中文簡稱:雪花算法) 是 Twitter 內部的一個 ID 生算法,可以通過一些簡單的規則保證在大規模分布式情況下生成唯一的 ID 號碼。Snowflake把 64-bit分別劃分成多段,分開來標識機器ID、時間等。其核心思想是:使用 41bit作為毫秒數,10bit作為機器的ID(5個bit是數據中心,5個bit的機器ID),12bit作為毫秒內的流水號,最后還有一個符號位,永遠是0。SnowFlake 的結構圖如下所示:
可以很清晰的看出,Snowflake 由 4個部分組成:
- 第一部分:bit 值,為未使用的符號位
- 第二部分:由 41 位的時間戳(毫秒)構成,它的取值是當前時間相對于某一時間的偏移
- 第三部分:表示工作機器 id,由服務節點 id 和數據中心 id 組合而成
- 第四部分:表示每個工作機器每毫秒生成的序列號 ID,同一毫秒內最多可生成生產 4095 個 ID。
由于在 Java 中 64bit 的整數是 long 類型,因此在 Java 中 SnowFlake 算法生成的 id 就是 long 來存儲的。
SnowFlake 算法可以保證:
- 1.所有生成的 id 按時間趨勢遞增
- 2.整個分布式系統內不會產生重復id(因為有服務節點 id 和數據中心 id 來做區分)
需要注意的是:
- 在分布式環境中,5 個 bit 位的 datacenter 和 worker 表示最多能部署 31 個數據中心,每個數據中心最多可部署 31 臺節點。
- 41 位的二進制長度最多能表示
2^41 -1
毫秒即 69 年,所以雪花算法最多能正常使用 69 年,為了能最大限度的使用該算法,在使用的時候,應該為其指定一個開始時間,不然會發生重復!
優點
- 高并發下的唯一性:由于算法設計考慮到了時間戳、機器標識等因素,因此即使是在大規模分布式系統中也能保證生成的ID是唯一的。
- 趨勢遞增:生成的ID通常是按照時間順序遞增的,這有利于數據庫索引性能優化。
- 信息量豐富:ID中包含了時間戳、工作節點ID等信息,使得每個ID都攜帶了額外的信息價值。
- 不依賴數據庫:相比于傳統基于數據庫自增ID的方式,雪花算法不需要訪問數據庫即可生成ID,減少了數據庫的壓力。
- 高效:計算效率高,適合快速生成大量ID的需求。
缺點
- 時鐘回撥問題:如果服務器時鐘出現回撥,則可能產生重復ID的問題。不過,可以通過等待時鐘追趕上來或者拒絕服務來解決這個問題。
- 有限的空間:雖然64位足夠大,但理論上還是存在上限,對于某些極端情況可能不夠用。
- 可讀性差:生成的ID對人類來說不易于閱讀和理解。
- 安全性較低:因為包含時間戳,所以可能泄露一些關于數據創建時間的信息,這在某些安全敏感的應用場景下可能是不利的。
適用場景
- 分布式系統:特別適用于需要跨多個服務器或數據中心工作的應用程序,如電商網站、社交平臺等。
- 高并發應用:當系統需要處理大量的并發請求并要求快速響應時,比如在線游戲、實時消息傳遞服務等。
- 需要唯一標識符的服務:任何需要生成全局唯一標識符的服務都可以采用此方法,例如訂單管理系統、用戶賬號注冊等。
- 大數據分析:由于ID帶有時間信息,有助于進行時間序列數據分析。
方案四:借助Redis,分布式組件
要想在分布式環境下生成一個唯一的訂單編號,我們可以通過分布式組件的方式,來幫忙我們生成全局唯一的訂單號,例如我們可以采用 redis 分布式緩存組件中的incr
命令,來幫我們生成一個全局自增長的序列號。
實現某個Key實現自增的代碼如下:
// 基于某個key實現自增長
String res = jedis.get(key);
if (StringUtils.isBlank(res)) {// 設置初始值,INIT_ID 是初始值jedisClient.set(key, INIT_ID);// 設置過期時間,seconds 是多少秒過期jedisClient.expire(key, seconds);
}
//存在就生成+1的訂單號
long orderId = jedis.incr(key);
這種方式生成的自增長序列號,非常的快,可以很好的滿足大流量環境下的編號要求唯一的特性!
案例分析:
在互聯網幾個大廠的訂單號分析一下:
-
京東商城訂單號格式:157444499
-
蘇寧易購訂單號格式:2000839647
-
凡客誠品訂單號格式:213052230059
-
小米訂單號格式:1111218032345170
凡客誠品和銀泰網訂單號都含有 0522,這是因為這 2 張訂單都是2013年5月22號下的訂單。
基本猜測一下,凡客的訂單規則是:業務編碼+年的后2位+月+日+訂單數;泰網的訂單號規則:年的第三位數+業務編碼+年的后1位+月+日+訂單數;而京東商城和蘇寧易購的訂單號看不出規則。
再來分析一下小米訂單編號:
1211218032345170(16位)//拆解成為四個部分
1——211218—03234—5170
- 第一部分,1 表示購買,2 表示退貨。
- 第二部分,表示 2021 年 12 月 18 日下的單,前面兩位省掉了。
- 第三部分,時間戳對應
00:53:54
,換算成秒是03234
秒。 - 最后一部分,表示在同一秒內下的第 5170 單,也就是說,小米認為,在一秒內不會超過一萬個訂單。
總結
通過上面的示例演示,下面針對這幾種情況做一個分析與總結。盡可能的選擇一種合理的方式。
實現方案 | 優勢 | 劣勢 |
---|---|---|
數據庫自增 | 代碼層面無需任何特殊處理;利用MySQL特點實現數據遞增 | 并發性能差;MySQL負擔重 |
UUID | 實現簡單、方便;重復性低 | 可讀性低;過于冗長;數據庫查詢效率低 |
雪花算法 | 基于內存、速度快;性能高;不會產生額外的網絡開銷;數據依次成遞增 | 依賴于服務器時間,如變動服務器時間則存在重復的情況 |
Redis | 基于內存、速度庫;使用簡單;可分布數據、擴展性強 | 需要獨立搭建一套服務、增加了維護成本;跨應用調用、存在網絡開銷 |
總體上來說,優先選擇使用Redis進行分布式的處理方式,如果在沒有Redis的情況下,那就優先選用雪花算法。
如果想要設計一個訂單號,需要保留業務類型、時間信息等。
比如通過2位數字表示業務類型,如交易訂單、支付單、結算單等都是不同的業務類型,可以有不同的編號。
中間的18-20位用一個唯一的ID來表示,可以用雪花算法,也可以用Leaf,總之就是他需要保證唯一性。
最后4位,基于基因法,將分表后的結果獲取到,把他也編碼到訂單號中。