JDK在1.5版本以後,提供了java.util.concurrent包,其中java.util.concurrent.atomic子包中包含了對於單一變量的線程安全的支持lock-free的編程實現。該包中的類,好比AtomicLong
,提供了和Long類型相對應的原子化操做,好比一些increment方法,基於這些功能,是能夠開發出單JVM的序列生成器這樣的功能的,可是對於分佈式環境,則無能爲力。 在Ignite中,除了提供標準的基於鍵-值的相似於Map的存儲之外,還提供了一種分佈式數據結構的實現,其中包括:IgniteAtomicLong
,IgniteSet
,IgniteQueue
,IgniteAtomicReference
,IgniteAtomicSequence
,IgniteCountDownLatch
,IgniteSemaphore
,這些類除了提供和JDK相同的功能外,就是增長了對分佈式環境的支持,也就是支持集羣範圍內的原子化操做。 鑑於本文重點是討論分佈式ID生成器,因此下文的重點在於IgniteAtomicSequence。java
IgniteAtomicSequence接口提供了分佈式的原子性序列,相似於分佈式原子性的Long類型,可是他的值只能增加,他特有的功能是支持預留必定範圍的序列值,來避免每次序列獲取下一個值時都須要的昂貴的網絡消耗和緩存更新,也就是,當在一個原子性序列上執行了incrementAndGet()(或者任何其餘的原子性操做),數據結構會往前預留必定範圍的序列值,他會保證對於這個序列實例來講跨集羣的惟一性。 這個類型的使用是很是簡單的,相關代碼以下:數據庫
Ignite ignite = Ignition.start(); IgniteAtomicSequence seq = ignite.atomicSequence("seqName",//序列名 0, //初始值 true//若是序列不存在則建立 ); for (int i = 0; i < 20; i++) { long currentValue = seq.get();//獲取當前值 long newValue = seq.incrementAndGet();//先加1再取值 ... }
這個樣例中建立的seq,初始值從0開始,而後遞增,看上去很完美,可是當系統無論什麼緣由重啓後,就又會從0開始,這顯然是沒法保證惟一性的,所以這個方法仍是不能在生產環境下使用。編程
按照前述,直接按照初始值0建立IgniteAtomicSequence,是有很大風險的,沒法在生產環境下使用,並且存在長度不固定問題,因此還須要進一步想辦法,研究的重點在於解決初始值的問題。 由於IgniteAtomicSequence的值爲long型,而在Java中long類型的最大值是9223372036854775807,這個數值長度爲19位,對於實際應用來講,是一個很大的值,可是對於常見的沒有環境依賴的ID生成器來講,仍是比較短的。所以咱們打算在這方面作文章。 由於系統重置的一個重要指標就是時間,那麼咱們以時間做爲參照,而後加上一個擴展,多是一個比較理想的選擇,咱們以以下的規則做爲初始值: 時間的yyyyMMddHHmmss+00000 這個長度正好是19位,而後每次加1,由於如今是2016年,這個規則在常規應用場景中,是不會超過long類型的最大值的。 可是,這個規則存在一個風險,就是假設不考慮實際應用和實際性能,若是增長操做業務量特別大,會使這個序列值快速進位,若是某個時間節點宕機後瞬間重啓,是有可能存在重啓後的初始值小於原來的最大值的,這時就沒法保證惟一性了。下面就對這個理論狀況下的最大值作一個計算,而後開發者就會知道在本身的應用中如何改進這個規則以知足個性化需求了。緩存
假定不考慮實際性能,咱們以最簡單的狀況爲例,就是啓動後一秒鐘內訪問達到峯值,而後宕機後瞬間重啓這種狀況,這個很容易就能看出來,不須要計算,就是5個0對應的最大值10萬,以此類推,考慮到時間的進位和十進制進位的不一樣,咱們能夠計算出一分鐘後、一小時後、一天後、一月後、一年後宕機換算出的交易量的極大值,以下:安全
每秒 | 每分 | 每小時 | 天天 | 每個月 | 每一年 | |
---|---|---|---|---|---|---|
1秒 | 10萬 | - | - | - | - | - |
1分 | 16.6萬 | 1000萬 | - | - | - | - |
1小時 | 27.7萬 | 1666萬 | 10億 | - | - | - |
1天 | 115萬 | 6944萬 | 41.66億 | 1000億 | - | - |
1月 | 165萬 | 9920萬 | 59.5億 | 3571.43億 | 10萬億 | - |
1年 | 3215萬 | 19億 | 1157億 | 2.7萬億 | 83萬億 | 1000萬億 |
以1分鐘爲例進行說明,假設初始值爲2016011815341200000,一分鐘後宕機瞬間重啓,對應的初始值爲2016011815351200000,這個差額是10000000,對應的每秒交易量爲16.6萬。 從上圖來看,對於這樣的規則,能承載的交易量仍是很大的,當今世界最繁忙的交易系統,也不會超過這個極限狀況下的極值,也就是說,這個規則就目前來講,具備廣泛適用性。 而在實際生產中,瞬間重啓是不存在的,隨着重啓時間向後推移,新的初始值會和原來的最大值拉開差距,更不可能出現衝突了。 關於性能,我在一臺2011年的舊筆記本上進行測試,很容易就能達到50K/s的序列生成速度,這個仍是能夠的,可是這是在開啓預留的前提下實現的,若是不開啓預留,性能可能降低到13K/s。在一個具體的集羣環境下,一般不會拿Ignite單獨創建服務作ID分發中心,因此實際環境下性能能不能知足需求,開發者須要自行進行測試,評估而後作選擇。另外,開啓了預留會致使最終生成的ID可能不是隨時間線性增加的,這個也須要注意。網絡
前述的基於Ignite的分佈式ID生成器,優勢是實現簡單,將一個jar包嵌入應用後ID生成系統和應用生命週期一致,設置了備份後不存在單點故障,數值線性遞增可比較大小,規則按照業務定製後能夠作得更短,若是轉成十六進制後,會很是短,不依賴數據庫,不對數據庫產生壓力,缺點可能就是性能以及一些特定的業務需求了。 生成全局惟一ID的需求是剛性的,尤爲是分佈式環境中,問題顯得尤其複雜。當前,這方面的實現方案很是多,通用的不通用的,本文不作詳細的論述,只作簡單的列舉:數據結構
分佈式ID生成策略有不少的實現方案,各有優缺點,本文又提出了一個基於Apache Ignite的新方案,應該說沒有最完美的,只有最符合實際業務需求的,開發者須要作的就是作詳細的、綜合的比較,而後選擇最適合本身的方案。分佈式