1. 程式人生 > >分布式ID生成

分布式ID生成

分布式ID 分布式系統

在看代碼的時候遇到一個snowflake算法,查了一下發現是Twitter的一個分布式ID生成算法,能夠在分布式環境中生成一個全局唯一的ID,然後上網找了一些業界的做法,目前看到了攜程和美團的方案,做一下筆記。

背景1

在復雜分布式系統中,往往需要對大量的數據和消息進行唯一標識。如在美團點評的金融、支付、餐飲、酒店、貓眼電影等產品的系統中,數據日漸增長,對數據分庫分表後需要有一個唯一ID來標識一條數據或消息,數據庫的自增ID顯然不能滿足需求;特別一點的如訂單、騎手、優惠券也都需要有唯一ID做標識。此時一個能夠生成全局唯一ID的系統是非常必要的。
概括下來,那業務系統對ID號的要求有哪些呢?

需求1

1、全局唯一性:不能出現重復的ID號,既然是唯一標識,這是最基本的要求。
2、趨勢遞增:在MySQL InnoDB引擎中使用的是聚集索引,由於多數RDBMS使用B-tree的數據結構來存儲索引數據,在主鍵的選擇上面我們應該盡量使用有序的主鍵保證寫入性能。
3、單調遞增:保證下一個ID一定大於上一個ID,例如事務版本號、IM增量消息、排序等特殊需求。
4、信息安全:如果ID是連續的,惡意用戶的扒取工作就非常容易做了,直接按照順序下載指定URL即可;如果是訂單號就更危險了,競對可以直接知道我們一天的單量。所以在一些應用場景下,會需要ID無規則、不規則。
上述123對應三類不同的場景,3和4需求還是互斥的,無法使用同一個方案滿足。

同時除了對ID號碼自身的要求,業務還對ID號生成系統的可用性要求極高,想象一下,如果ID生成系統癱瘓,整個美團點評支付、優惠券發券、騎手派單等關鍵動作都無法執行,這就會帶來一場災難。

由此總結下一個ID生成系統應該做到如下幾點:
全局唯一
支持高並發
能夠體現一定屬性
高可靠,容錯單點故障
高性能

平均延遲和TP999延遲都要盡可能低;
可用性5個9;
高QPS。

業界方案1

UUID
UUID(Universally Unique Identifier)的標準型式包含32個16進制數字,以連字號分為五段,形式為8-4-4-4-12的36個字符,示例:550e8400-e29b-41d4-a716-446655440000,到目前為止業界一共有5種方式生成UUID,詳情見IETF發布的UUID規範 A Universally Unique IDentifier (UUID) URN Namespace。

優點

性能非常高:本地生成,沒有網絡消耗。
缺點:

不易於存儲:UUID太長,16字節128位,通常以36長度的字符串表示,很多場景不適用。
信息不安全:基於MAC地址生成UUID的算法可能會造成MAC地址泄露,這個漏洞曾被用於尋找梅麗莎病×××者位置。
ID作為主鍵時在特定的環境會存在一些問題,比如做DB主鍵的場景下,UUID就非常不適用:

① MySQL官方有明確的建議主鍵要盡量越短越好[4],36個字符長度的UUID不符合要求。

All indexes other than the clustered index are known as secondary indexes. In InnoDB, each record in a secondary index contains the primary key columns for the row, as well as the columns specified for the secondary index. InnoDB uses this primary key value to search for the row in the clustered index. If the primary key is long, the secondary indexes use more space, so it is advantageous to have a short primary key.

② 對MySQL索引不利:如果作為數據庫主鍵,在InnoDB引擎下,UUID的無序性可能會引起數據位置頻繁變動,嚴重影響性能。

類snowflake方案
這種方案大致來說是一種以劃分命名空間(UUID也算,由於比較常見,所以單獨分析)來生成ID的一種算法,這種方案把64-bit分別劃分成多段,分開來標示機器、時間等,比如在snowflake中的64-bit分別表示如下圖(圖片來自網絡)所示:

image

41-bit的時間可以表示(1L<<41)/(1000L360024*365)=69年的時間,10-bit機器可以分別表示1024臺機器。如果我們對IDC劃分有需求,還可以將10-bit分5-bit給IDC,分5-bit給工作機器。這樣就可以表示32個IDC,每個IDC下可以有32臺機器,可以根據自身需求定義。12個自增序列號可以表示2^12個ID,理論上snowflake方案的QPS約為409.6w/s,這種分配方式可以保證在任何一個IDC的任何一臺機器在任意毫秒內生成的ID都是不同的。

這種方式的優缺點是:

優點:

毫秒數在高位,自增序列在低位,整個ID都是趨勢遞增的。
不依賴數據庫等第三方系統,以服務的方式部署,穩定性更高,生成ID的性能也是非常高的。
可以根據自身業務特性分配bit位,非常靈活。
缺點:

強依賴機器時鐘,如果機器上時鐘回撥,會導致發號重復或者服務會處於不可用狀態。
應用舉例Mongdb objectID
MongoDB官方文檔 ObjectID可以算作是和snowflake類似方法,通過“時間+機器碼+pid+inc”共12個字節,通過4+3+2+3的方式最終標識成一個24長度的十六進制字符。

數據庫生成
以MySQL舉例,利用給字段設置auto_increment_increment和auto_increment_offset來保證ID自增,每次業務使用下列SQL讀寫MySQL得到ID號。

begin;
REPLACE INTO Tickets64 (stub) VALUES (‘a‘);
SELECT LAST_INSERT_ID();
commit;
image

這種方案的優缺點如下:

優點:

非常簡單,利用現有數據庫系統的功能實現,成本小,有DBA專業維護。
ID號單調自增,可以實現一些對ID有特殊要求的業務。
缺點:

強依賴DB,當DB異常時整個系統不可用,屬於致命問題。配置主從復制可以盡可能的增加可用性,但是數據一致性在特殊情況下難以保證。主從切換時的不一致可能會導致重復發號。
ID發號性能瓶頸限制在單臺MySQL的讀寫性能。
對於MySQL性能問題,可用如下方案解決:在分布式系統中我們可以多部署幾臺機器,每臺機器設置不同的初始值,且步長和機器數相等。比如有兩臺機器。設置步長step為2,TicketServer1的初始值為1(1,3,5,7,9,11...)、TicketServer2的初始值為2(2,4,6,8,10...)。這是Flickr團隊在2010年撰文介紹的一種主鍵生成策略(Ticket Servers: Distributed Unique Primary Keys on the Cheap )。如下所示,為了實現上述方案分別設置兩臺機器對應的參數,TicketServer1從1開始發號,TicketServer2從2開始發號,兩臺機器每次發號之後都遞增2。

TicketServer1:
auto-increment-increment = 2
auto-increment-offset = 1

TicketServer2:
auto-increment-increment = 2
auto-increment-offset = 2
假設我們要部署N臺機器,步長需設置為N,每臺的初始值依次為0,1,2...N-1那麽整個架構就變成了如下圖所示:

image

這種架構貌似能夠滿足性能的需求,但有以下幾個缺點:

系統水平擴展比較困難,比如定義好了步長和機器臺數之後,如果要添加機器該怎麽做?假設現在只有一臺機器發號是1,2,3,4,5(步長是1),這個時候需要擴容機器一臺。可以這樣做:把第二臺機器的初始值設置得比第一臺超過很多,比如14(假設在擴容時間之內第一臺不可能發到14),同時設置步長為2,那麽這臺機器下發的號碼都是14以後的偶數。然後摘掉第一臺,把ID值保留為奇數,比如7,然後修改第一臺的步長為2。讓它符合我們定義的號段標準,對於這個例子來說就是讓第一臺以後只能產生奇數。擴容方案看起來復雜嗎?貌似還好,現在想象一下如果我們線上有100臺機器,這個時候要擴容該怎麽做?簡直是噩夢。所以系統水平擴展方案復雜難以實現。
ID沒有了單調遞增的特性,只能趨勢遞增,這個缺點對於一般業務需求不是很重要,可以容忍。
數據庫壓力還是很大,每次獲取ID都得讀寫一次數據庫,只能靠堆機器來提高性能。

4、Redis生成ID [貌似我們用的這個]

當使用數據庫來生成ID性能不夠要求的時候,我們可以嘗試使用Redis來生成ID。這主要依賴於Redis是單線程的,所以也可以用生成全局唯一的ID。可以用Redis的原子操作INCR和INCRBY來實現。

可以使用Redis集群來獲取更高的吞吐量。假如一個集群中有5臺Redis。可以初始化每臺Redis的值分別是1,2,3,4,5,然後步長都是5。各個Redis生成的ID為:

A:1,6,11,16,21

B:2,7,12,17,22

C:3,8,13,18,23

D:4,9,14,19,24

E:5,10,15,20,25

比較適合使用Redis來生成每天從0開始的流水號。比如訂單號=日期+當日自增長號。可以每天在Redis中生成一個Key,使用INCR進行累加。

優點:

不依賴於數據庫,靈活方便,且性能優於數據庫。

數字ID天然排序,對分頁或者需要排序的結果很有幫助。

使用Redis集群也可以防止單點故障的問題。

缺點:

如果系統中沒有Redis,還需要引入新的組件,增加系統復雜度。

需要編碼和配置的工作量比較大,多環境運維很麻煩,

在開始時,程序實例負載到哪個redis實例一旦確定好,未來很難做修改。

6.還有其他一些方案,比如京東淘寶等電商的訂單號生成。因為訂單號和用戶id在業務上的區別,訂單號盡可能要多些冗余的業務信息,比如:

滴滴:時間+起點編號+車牌號

淘寶訂單:時間戳+用戶ID

其他電商:時間戳+下單渠道+用戶ID,有的會加上訂單第一個商品的ID。

而用戶ID,則要求含義簡單明了,包含註冊渠道即可,盡量短。

總結一下:
1、競對竟然還可以通過分析訂單號得到大約一天得量。。。
2、snowflake的問題是時鐘回退問題。。。

source1--美團
source2--攜程

分布式ID生成