1. 程式人生 > >分散式全域性唯一ID的實現

分散式全域性唯一ID的實現

分散式全域性唯一ID的實現

前言

上週末考完試,這周正好把工作整理整理,然後也把之前的一些素材,整理一番,也當自己再學習一番。
一方面正好最近看到幾篇這方面的文章,另一方面也是正好工作上有所涉及,所以決定寫一篇這樣的文章。
先是簡單介紹概念和現有解決方案,然後是我對這些方案的總結,最後是我自己專案的解決思路。

概念

在複雜分散式系統中,往往需要對大量的資料和訊息進行唯一標識。

如在金融、電商、支付、等產品的系統中,資料日漸增長,對資料分庫分表後需要有一個唯一ID來標識一條資料或訊息,資料庫的自增ID顯然不能滿足需求,此時一個能夠生成全域性唯一ID的系統是非常必要的。

特點:

  • 全域性唯一性(核心):作為唯一標識,不可以出現重複ID
  • 趨勢遞增:在MySQL InnoDB引擎中使用的是聚集索引,由於多數RDBMS使用B-tree的資料結構來儲存索引資料,在主鍵的選擇上面我們應該儘量使用有序的主鍵保證寫入效能。
  • 單調遞增:保證下一個ID一定大於上一個ID,例如事務版本號、IM增量訊息、排序等特殊需求。
  • 資訊保安:如果ID是連續的,惡意使用者的扒取工作就非常容易做了,直接按照順序下載指定URL即可;如果是訂單號就更危險了,競對可以直接知道我們一天的單量。所以在一些應用場景下,會需要ID無規則、不規則。
    同時除了對ID號碼自身的要求,業務還對ID號生成系統的可用性要求極高,想象一下,如果ID生成系統癱瘓,這就會帶來一場災難。

運用場景:

分散式全域性唯一ID(資料庫的分庫分表後需要有一個唯一ID來標識一條資料或訊息;特別一點的如訂單、騎手、優惠券也都需要有唯一ID做標識;MQ中訊息的高可用性(確認訊息是否傳送成功,是否已傳送等)等)
其實分散式全域性ID是一個比較複雜,重要的分散式問題(什麼問題涉及真正的分散式,高併發後都會比較複雜)。常見解決方案有UUID,Snowflake,Flicker,Redis,Zookeeper,Leaf等。

實現方案:

UUID(此處用的Version1:共五個版本,Version1是基於時間的)

生成一個32位16進位制字串(16位元組的128位資料,通常以32位長度的字串表示)(結合機器識別碼(全域性唯一的IEEE機器識別號,如果有網絡卡,從網絡卡MAC地址獲得,沒有網絡卡以其他方式獲得),當前時間,一個隨機數)。

優點:

  • 效能好;
  • 擴充套件性高;
  • 本地生成;
  • 無網路消耗;
  • 不需要考慮效能瓶頸;
  • 不需要提前商定,各自為政,但絕對不會衝突

缺點:

  • 無法保證趨勢遞增(由於資料庫MySQL的InnoDB採用聚簇索引,有序的ID可以保證寫入速度);
  • UUID過長(消耗記憶體,頻寬等。更重要的是如果儲存在資料庫中,作為主鍵建立索引效率低)

適用場景:

不需要考慮空間佔用,不需要生成有遞增趨勢的ID,且不在MySQL中儲存。

Snowflake

Twitter開源,生成一個64bit(0和1)字串(1bit不用,41bit表示儲存時間戳,10bit表示工作機器id(5位資料標示位,5位機器標識位),12bit序列號)

結構:

  • 首位符號位:因為ID一般為正數,該值為0.
  • 41位時間戳(毫秒級):時間戳並不是當前時間戳,而是儲存時間戳的差值(當前時間戳-起始時間戳(起始時間戳需要程式指定),理論可以適用(1<<41)/(1000x60x60x24x365),69年。
  • 10位資料機器位(說白了就是邏輯分片ID,具體實現和機器本身無關係):包括5位資料標識位和5位機器標識位(比如5位機房ID,5位機器ID),理論最多可以部署節點位:1<<10=1024。
  • 12位毫秒內的序列:同一節點,同一時刻(同一毫秒內)最多生成ID數1<<12=4096。

最後生成64位Long型數值(這裡指,一般Long資料就是64位bit的)。

優點:

  • 趨勢遞增,且按照時間有序;
  • 效能高,穩定性高,不依賴資料庫等第三方系統;
  • 可以按照自身業務特性靈活分配bit位(比如機器位改為15bit,序列位改為7bit)。

缺點:

  • 依賴機器時鐘(雖然UUID也根據當前時間,但其非時間部分波動太大了(重新組織措辭)),時鐘回撥會造成暫不可用或重複發號(分散式系統中,每臺機器上的時鐘不可能完全同步。在同步各個伺服器的時間時,有一定機率發生時鐘回撥(時間超了,往回撥))

適用場景:

要求高效能,可以不連續,資料型別為long型。

Flicker

主要思路是涉及單獨的庫表,利用資料庫的自增ID+replace_into,來生成全域性ID。

前置補充:

replace into跟insert功能類似,不同點在於:replace into首先嚐試插入資料列表中,如果發現表中已經有此行資料(根據主鍵或唯一索引判斷)則先刪除,再插入。否則直接插入新資料。

建表:
    create table t_global_id(
            id bigint(20) unsigned not null auto_increment,
            stub char(1) not null default '',
            primary key (id),
            unique key stub (stub)
    ) engine=MyISAM;
(stub:票根,對應需要生成ID的業務方編碼,可以是專案名,表名,甚至是伺服器IP地址。
  MyISAM(MYSQL5.5.8前預設資料庫儲存引擎,5.5.8及之後預設儲存引擎為InnoDB):(此處應當有MyISAM與InnoDB引擎的區別,乃至其他引擎)基於ISAM型別。不是事務安全(沒有事務隔離??),不支援外來鍵,沒有行級鎖。如果執行大量的select,建議MyISAM。

獲取資料:
    # 每次業務可以使用以下SQL讀寫MySQL得到ID號
    replace into t_golbal_id(stub) values('a');
    select last_insert_id();

擴充套件:為解決單點問題,啟用多臺伺服器,如MySQL,利用給欄位設定auto_increment_increment和auto_increment_offset來保證ID自增(如通過設定起始值與步長,生成奇偶數ID)

優點:

  • 非常簡單,充分利用了資料庫系統的功能實現,成本小,有DBA專業維護;
  • ID號單調自增,可以實現一些對ID有特殊要求的業務。

缺點:

  • 強依賴DB,當DB異常時,整個系統不可用,屬於致命問題(配置主從複製可以儘可能地增加可用性,但是資料一致性在特殊情況下難以保證。主從切換時的不一致可能導致重複發號);
  • 水平擴充套件困難(定義好了起始值,步長和機器臺數之後,如果要新增機器就比較麻煩(為什麼我想到了REDIS的雜湊一致原理));
  • ID發號效能瓶頸限制在單臺MySQL的讀寫效能。

適用場景:

資料量不大,併發量不大。

Redis

由於Redis的所有命令是單執行緒的,所以可以利用Redis的原子操作INCR和INCRBY,來生成全域性唯一的ID。

擴充套件:

可以通過叢集來提升吞吐量(可以通過為不同Redis節點設定不同的初始值並同意步長,從而利用Redis生成唯一且趨勢遞增的ID)(其實這個方法和Flicker一致,只是利用到了Redis的一些特性,如原子操作,記憶體資料庫讀寫快等)(Incrby:將key中儲存的數字加上指定的增量值。這是一個“INCR AND GET”的原子操作,業務方可以定義一個自己的key值,通過INCR命令來獲取對應的ID)

優點:

不依賴資料庫,靈活方便,且效能優於基於資料庫的Flicker方案。

缺點:

  • 擴充套件性低,Redis叢集需要設定號初始值與步長(與Flicker方案一樣);
  • Redis宕機可能生成重複的ID;如果系統中沒有Redis,還需要引入新的元件,增加系統複雜度;
  • 需要編碼和配置的工作量比較大。

適用場景:

Redis叢集高可用,併發量高。

舉例:

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

zookeeper

通過其znode資料版本來生成序列號,可以生成32位和64位的資料版本號,客戶端可以使用這個版本號來作為唯一的序列號。

小結:很少會使用zookeeper來生成唯一ID。主要是由於需要依賴zookeeper,並且是多步呼叫API,如果在競爭較大的情況下,需要考慮使用分散式鎖。因此,效能在高併發的分散式環境下,也不甚理想。

Leaf

美團的Leaf分散式ID生成系統,在Flicker策略與Snowflake演算法的基礎上做了兩套優化的方案:Leaf-segment資料庫方案(相比Flicker方案每次都要讀取資料庫,該方案改用proxy server批量獲取,且做了雙buffer的優化)與Leaf-snowflake方案(主要針對時鐘回撥問題做了特殊處理。若發生時鐘回撥則拒絕發號,並進行告警)。

MongDB objectID

ObjectID可以算作和snowflake類似方法,通過”時間+機器碼+pid+inc”共12個位元組,通過4+3+2+3的方式,最終標識一個24長度的十六進位制字元。

理論總結

其實除了上述方案外,還有ins等的方案,但總的來看,方案主要分為兩種:第一有中心(如資料庫,包括MYSQL,REDIS等),其中可以會利用事先的預約來實現叢集(起始步長)。第二種就是無中心,通過生成足夠散落的資料,來確保無衝突(如UUID等)。站在這兩個方向上,來看上述方案的利弊就方便多了。

中心化方案:

優點:

  • 資料長度相對小一些;
  • 資料可以實現自增趨勢等。

缺點:

  • 併發瓶頸處理;
  • 叢集需要實現約定;橫向擴充套件困難(當然有的方案看起來後兩者沒有那麼問題,是因為,這些方案利用其技術特性,早就一定程度上解決了這些問題,如Redis的橫向擴充套件等)。

非中心化方案:

優點:

  • 實現簡單(因為不需要與其他節點存在這方面的約定,耦合);
  • 不會出現中心節點帶來的效能瓶頸;
  • 擴充套件性較高(擴充套件的侷限往往集中於資料的離散問題)。

缺點:

  • 資料長度較長(畢竟就是通過這一特性來實現無衝突的);
  • 無法實現資料的自增長(畢竟是隨機的);
  • 依賴資料生成方案的優劣(資料生成方案的優劣會全盤接收,但可以推成出新)。

體悟:

技術是無窮無盡的,我們不僅需要看到其中體現的思想與原則,在學習新技術或方案時,需要明確其中一些特性,優缺點的來源,從而進行有效的總結歸納。

應用角度來說:(一方面想要標示符短,便於處理與儲存,另一方面想要足夠大,而不會產生衝突。呵呵)。最理想就是追求從0開始,每個標示符都被使用,且不重複,而且不用擔心併發。呵呵。完全應該根據當前業務場景來選擇,畢竟業務場景在當前是確定的。如果業務變動較大(比如發展初期,業務增長很快),那就需要考慮擴充套件性,便於日後進行該模組的更新與技術方案的替換實現(避免一個系統開發一年,用不到一年,那就尷尬了))。

個人經驗

我曾經做過一個“工業物聯網”系統,該系統系統是分為三個子系統:終端伺服器(用於收集終端感測器資料);企業中控伺服器(接收來自多個終端伺服器的資料,進行綜合檢視與控制);雲平臺伺服器(提供上雲)。其中就涉及多個終端伺服器的感測器資料辨識問題,這裡以傾斜感測器資料為例。簡述不同終端伺服器的傾斜資料的如何實現全域性唯一標識。

以企業中控伺服器的資料庫作為統一的資料標識來源

簡單說,就是終端伺服器傳送一個數據到企業中控室,企業中控伺服器就將該資料儲存到資料庫中,那麼每個資料在企業中控伺服器資料庫中都有唯一的ID,並且保持了自增。

優點是實現簡單,只需要做好資料收發,與資料的插入工作即可。唯一需要注意的是資料庫插入時注意資源互斥,防止出現數據插入異常問題(Springframework生成的Bean預設時單例的)。

缺點是需要實時收發資料,防止資料丟失,資料積壓,資料的create_time異常等問題。

以UUID等方式生成資料的全域性唯一標識

簡單說,就是終端伺服器要傳送的資料賦予UUID這樣的ID,來確保全域性唯一。這樣終端伺服器就可以和中控伺服器保持同樣且不衝突的ID了。資料的生成是實現在終端伺服器的,而中控伺服器只是作為資料的儲存與呼叫(通過統一ID呼叫)。

優點是不需要資料的實時收發,避免系統在弱網路情況下出現各類異常。

缺點是資料的ID過長,並且無法保持自增。並且在某種程度上帶來了資料複雜度,從而提高了系統複雜度。

落地方案

由於實際業務的需求,如弱網路,資料互動頻率跨度大等情況。最終我的實現是先由終端伺服器在啟動之初,在企業中控伺服器註冊TerminalId,作為不同終端伺服器的標識。不同終端伺服器接收與儲存資料時,都會在每條資料中插入TerminalId,便於企業中控伺服器的識別。當然,具體實現當中還有一些細節。如終端伺服器在註冊時由於網路等情況註冊失敗,會先建立一個類似UUID的TerminalId來先儲存監測資料。當註冊成功時(系統會根據TerminalId的長度等特性來判斷是否註冊失敗,是否需要重新註冊),會重新修改所有資料的TerminalId,再允許資料上傳。

優點是確保了資料在弱網路情況下的正確性,並且實現了自動註冊等通用模組的實現。

缺點是最終資料插入企業中控伺服器資料庫時,並沒有嚴格實現資料符合實際時間的增長(如某終端伺服器由於網路等情況沒法傳送資料,等待一段時間後傳送了這段時間堆積的資料),但保持了總體增長的趨勢。

總結

IT沒有銀彈,我們要做的是多去了解現有的技術方案,再產生符合自己需求的技術方案。因為不同的技術方案都因為其使用場景有著各自的特點,而我們需要了解各種特點的技術來源(是什麼技術造就了這一特點,或者說是什麼架構造就了這一特點等),從而構建出最符合自己需求的技術方案。

沒有最好,只有最適合。

附錄

參考

阿里P8架構師談:分散式系統全域性唯一ID簡介、特點、5種生成方式

分散式ID生成策略

分散式全域性唯一ID生成策