分佈式全局惟一ID的實現

分佈式全局惟一ID的實現

前言

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

概念

在複雜分佈式系統中,每每須要對大量的數據和消息進行惟一標識。redis

如在金融、電商、支付、等產品的系統中,數據日漸增加,對數據分庫分表後須要有一個惟一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等。sql

實現方案:

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沒有銀彈,咱們要作的是多去了解現有的技術方案,再產生符合本身需求的技術方案。由於不一樣的技術方案都由於其使用場景有着各自的特色,而咱們須要瞭解各類特色的技術來源(是什麼技術造就了這一特色,或者說是什麼架構造就了這一特色等),從而構建出最符合本身需求的技術方案。

沒有最好,只有最適合。

相關文章
相關標籤/搜索