在幾乎全部的分佈式系統或者採用了分庫/分表設計的系統中,幾乎都會須要生成數據的惟一標識ID的需求,java
常規作法,是使用數據庫中的自動增加列來作系統主鍵,可是這樣的作法沒法保證ID全局惟一.git
那麼一個分佈式ID生成器應該知足那些需求呢 :redis
全局惟一性算法
趨勢遞增spring
可以融入分庫基因數據庫
本文將基於snowflake的算法來進行如下的討論,固然,分佈式ID的生成方案有不少,緩存
不過在本文並不會分散開來討論/比對,由於網上相關的文章實在太多,若是有須要瞭解的同窗,請自行百度.架構
同時,也不會討論snowflake算法,一樣也是由於網上相關的文章實在太多,若是有須要瞭解的同窗,請自行百度.併發
先看兩段代碼:負載均衡
public void id() { Map<Long, Long> map = new HashMap<>(); int maxCount = 100; IdWorker idWorker = new IdWorker(1, 1); for (int i = 0; i < maxCount; i++) { long id = idWorker.nextId(); map.put(id, id); } log.info("{} , {}", maxCount, map.size()); }
輸出爲 : 100 , 100
public void id() { Map<Long, Long> map = new HashMap<>(); int maxCount = 100; for (int i = 0; i < maxCount; i++) { IdWorker idWorker = new IdWorker(1, 1); long id = idWorker.nextId(); map.put(id, id); } log.info("{} , {}", maxCount, map.size()); }
輸出爲 : 100 , 10
瞭解snowflake的同窗也都知道,這個算法是基於時間的,以下組成 :
0 | 時間(41位) | 數據中心ID(5位) | 機器ID(5位) | 序號(12位)
而生成ID的算法邏輯,簡單點說,在相同數據中心ID和機器ID的狀況下,若是時間的毫秒數是一致的,那麼就經過遞增序列號來保證ID不重複.
也就是說在1毫秒內最大生成的ID個數是二進制12bit的最大值,也就是4096(0-4095)個
那麼若是序列號超過了這個最大值,則會將程序阻塞到下一毫秒,而後序列號歸零,繼續生成ID.
好知道了生成ID的邏輯後,上面兩個程序判斷的現象也就不難解釋了.
程序一 : 沒有重複,是由於在整個循環中,ID生成器只實例化過一次,在循環的過程當中,能正常的遞增序列號,因此不會有重複的ID出現
程序二 : 有重複,是由於ID生成器是在循環中循環實例化的,每次生成ID的時候序列號都是0,可是程序執行很快,獲得的時間毫秒數又是同樣的,那麼,就必然會有重複值了.
因此從以上的程序片斷和分析中能夠得出一個結論 : 要想snowflake生成全局惟一的ID,那麼ID生成器必須也是全局單例的
兩個點要主注意一下 :
分佈式系統下全局靜態變量也是多份的,由於系統可能運行在不一樣的JVM下,並不能保證變量的全局單例
前面提到了在同一毫秒下,最多隻能生成4096個ID,對於那些併發量很大是系統來講,顯然是不夠的,
那麼這個時候就是經過datacenterId和workerId來作區分,這兩個ID,分別是5bit,共10bit,最大值是1024(0-1023)個,
在這種狀況下,snowflake一毫秒理論上最大可以生成的ID數量是約42W個,這是一個很是大的基數了,理論上可以知足絕大多數系統的併發量
因此得出一個結論 : snowflake能夠經過datacenterId和workerId來區分ID的歸屬(能夠是業務線,能夠是機房,等等,按需定義)來達到更大的ID生成數量
寫死 : 正如上面說的同樣,單機部署,而後寫死兩個值
讀配置文件 : 將值放在配置中心,應用啓動的時候讀取,而後初始化
動態分配 : 本文主旨
因此你們先看一下架構圖 :
#生成分佈式ID(按時間戳區分datacenterId和workerId) /service/id #生成分佈式ID(按dwId[0-1023]) /service/id/{dwId} #生成分佈式ID(按datacenterId[0-31]和workerId[0-31]) /service/id/{datacenterId}/{workerId} #批量生成分佈式ID(按時間戳區分datacenterId和workerId) /service/id/batch/{count} #批量生成分佈式ID(按dwId[0-1023]) /service/id/batch/{dwId}/{count} #批量生成分佈式ID(按datacenterId[0-31]和workerId[0-31]) /service/id/batch/{datacenterId}/{workerId}/{count}
在提供出來的rest服務中,提供了datacenterId和workerId的參數(dwId就是二者的融合,10bit),
總共預留了10個bit的空餘來支持分庫分表,最大支持1024個節點.
snowflake生成的ID是能夠被反解析的,這樣更進一步的支持了分庫的相關炒做,相關實現以下 :
Id reverseId = new Id(); reverseId.setSequence((id) & ~(-1L << 12)); // sequence reverseId.setDwId((id >> (12)) & ~(-1L << (10))); // dwId reverseId.setWorkerId((id >> 12) & ~(-1L << 5)); // workerId reverseId.setDatacenterId((id >> 17) & ~(-1L << 5)); // datacenterId reverseId.setTimestamp((id >> 22) + TWEPOCH); // timestamp return reverseId;
本方案是能夠支持ID生成服務有多個實例,最多1024個,能而且能保證每一個實例內,相同datacenterId和workerId的ID生成器只有一個,作到全局單例.
主要是經過redis原子鎖的來實現的.詳情可看上面的流程圖,主要分爲本地ID生成和跨實例ID生成兩種模式 :
這種狀況比較簡單,就是生成ID的請求剛剛落到ID生成器所在的實例上,而後就能夠直接拿到ID生成器,而後生成ID.
這種狀況簡單點說就是,好比你要生成3-3的ID,這個ID生成器在實例A上,可是負載均衡器將請求發到實例B上去了,
這個時候實例B上並無對應的ID生成器,這個時候,就會從緩存中拿到對應的緩存值,拿到用用這個ID生成器的HOST和PORT,
而後在作一個RMS請求,調用遠程的rest服務,生成ID,而後返回
上面提到了,ID生成器如今是全網單例的了,那麼其中一個節點有故障,掛掉了怎麼辦呢?
在跨實例ID生成的場景下,會有RMS請求失敗的狀況,遠程節點有可能會故障,這個時候,一旦RMS請求失敗,則會觸發故障轉移,
具體操做就是將redis中的對應緩存刪除掉,而後走一個實例化ID生成器的流程,這個時候,當前處理請求的節點就會將故障節點擁有的ID生成器轉移過來,轉爲本地生成模式,從而作到的故障轉移
若是是本地ID生成的話,那基本沒有性能損耗,直接操做本地變量.
跨實例ID生成的狀況會多出來一個RMS請求的耗時,可是一次ID生成的請求最多觸發一次RMS請求,消耗是可控的
在有節點故障的時候,觸發故障轉移會額外的產生一次ID實例化的流程,會形成輕微波動,但緊當前的這一次請求,下次的請求就會轉爲本地ID生成的模式
今天跟你們分享瞭如何動態分配snowflake的datacenterId和workerId,以及如何作到高可用的設計和思路,環境你們提出意見和建議
想得到最快更新,請關注公衆號