基於數據庫構建分佈式的ID生成方案

在分佈式系統中,生成全局惟一ID,有不少種方案,可是在這多種方案中,每種方案都有有缺點,下面咱們之針對經過經常使用數據庫來生成分佈式ID的方案,其它方法會在其它文中討論:mysql

1,RDBMS生成ID:

這裏咱們討論mysql生成ID。由於MySQL自己能夠auto_increment和auto_increment_offset來保證ID自增,很天然地,咱們會想到藉助這個特性來實現這個功能。算法

全局ID生成方案裏採用了MySQL自增加ID的機制(auto_increment + replace into + MyISAM)。一個生成64位ID方案具體實現是這樣的: 
先建立單獨的數據庫(eg:ticket),而後建立一個表:sql

CREATE TABLE Tickets64 (
id bigint(20) unsigned NOT NULL auto_increment,
stub char(1) NOT NULL default '',
PRIMARY KEY (id),
UNIQUE KEY stub (stub)
) ENGINE=MyISAM

表建立以後咱們要設置一個初始值,好比100000,執行SELECT * from Tickets64,查詢結果就是這樣的:mongodb

每當咱們的應用須要ID的時候就會作以下操做,調用以下存儲過程:數據庫

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

架構如圖:服務器

 

這樣咱們就能拿到不斷增加且不重複的ID了。 網絡

 

這種方案的優缺點以下:架構

優勢:併發

  • 很是簡單,利用現有數據庫系統的功能實現,成本小,有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那麼整個架構就變成了以下圖所示:

 

 

 

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

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

2,類snowflake方案

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

41-bit的時間能夠表示(1L<<41)/(1000L*3600*24*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相似方法:

爲了考慮分佈式,「_id」要求不一樣的機器都能用全局惟一的同種方法方便的生成它。所以不能使用自增主鍵(須要多臺服務器進行同步,既費時又費力),
所以選用了生成ObjectId對象的方法。

ObjectId使用12字節的存儲空間,其生成方式以下:

|0|1|2|3|4|5|6 |7|8|9|10|11|

|時間戳 |機器ID|PID|計數器 |

前四個字節時間戳是從標準紀元開始的時間戳,單位爲秒,有以下特性:

 1 時間戳與後邊5個字節一塊,保證秒級別的惟一性;
 2 保證插入順序大體按時間排序;
 3 隱含了文檔建立時間;
 4 時間戳的實際值並不重要,不須要對服務器之間的時間進行同步(由於加上機器ID和進程ID已保證此值惟一,惟一性是ObjectId的最終訴求)。

機器ID是服務器主機標識,一般是機器主機名的散列值。

同一臺機器上能夠運行多個mongod實例,所以也須要加入進程標識符PID。

前9個字節保證了同一秒鐘不一樣機器不一樣進程產生的ObjectId的惟一性。後三個字節是一個自動增長的計數器(一個mongod進程須要一個全局的計數器),保證同一秒的ObjectId是惟一的。同一秒鐘最多容許每一個進程擁有(256^3 = 16777216)個不一樣的ObjectId。

總結一下:時間戳保證秒級惟一,機器ID保證設計時考慮分佈式,避免時鐘同步,PID保證同一臺服務器運行多個mongod實例時的惟一性,最後的計數器保證同一秒內的惟一性(選用幾個字節既要考慮存儲的經濟性,也要考慮併發性能的上限)。

"_id"既能夠在服務器端生成也能夠在客戶端生成,在客戶端生成能夠下降服務器端的壓力。

3,使用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

這個,隨便負載到哪一個機肯定好,將來很難作修改。可是3-5臺服務器基本可以知足器上,均可以得到不一樣的ID。可是步長和初始值必定須要事先須要了。使用Redis集羣也能夠方式單點故障的問題。

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

 

優勢:

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

2)數字ID自然排序,對分頁或者須要排序的結果頗有幫助。

缺點:

1)若是系統中沒有Redis,還須要引入新的組件,增長系統複雜度。

2)須要編碼和配置的工做量比較大。

相關文章
相關標籤/搜索