spring boot / cloud (十六) 分布式ID生成服務


spring boot / cloud (十六) 分布式ID生成服務

在幾乎所有的分布式系統或者采用了分庫/分表設計的系統中,幾乎都會需要生成數據的唯一標識ID的需求,

常規做法,是使用數據庫中的自動增長列來做系統主鍵,但是這樣的做法無法保證ID全局唯一.

那么一個分布式ID生成器應該滿足那些需求呢 :

  • 全局唯一性

  • 趨勢遞增

  • 能夠融入分庫基因

本文將基於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生成器必須也是全局單例的

那申明一個全局靜態的ID生成器不就行了?

兩個點要主注意一下 :

  • 分布式系統下全局靜態變量也是多份的,因為系統可能運行在不同的JVM下,並不能保證變量的全局單例

  • 前面提到了在同一毫秒下,最多只能生成4096個ID,對於那些並發量很大是系統來說,顯然是不夠的,
    那么這個時候就是通過datacenterId和workerId來做區分,這兩個ID,分別是5bit,共10bit,最大值是1024(0-1023)個,
    在這種情況下,snowflake一毫秒理論上最大能夠生成的ID數量是約42W個,這是一個非常大的基數了,理論上能夠滿足絕大多數系統的並發量

所以得出一個結論 : snowflake可以通過datacenterId和workerId來區分ID的歸屬(可以是業務線,可以是機房,等等,按需定義)來達到更大的ID生成數量

那么有那些方法來分配atacenterId和workerId呢?

  • 寫死 : 正如上面說的一樣,單機部署,然后寫死兩個值

  • 讀配置文件 : 將值放在配置中心,應用啟動的時候讀取,然后初始化

  • 動態分配 : 本文主旨

所以本文主要討論的是如何動態分配snowflake的datacenterId和workerId,以及如何做到高可用

所以大家先看一下架構圖 :

分布式ID-邏輯架構示意

分布式ID-邏輯架構示意

分布式ID-發號流程示意

分布式ID-發號流程示意

相關源碼可在本文末尾的配套代碼倉庫中獲得,工程是 : udf-starter-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個節點.

反解析分布式ID

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生成器

本方案是可以支持ID生成服務有多個實例,最多1024個,能並且能保證每個實例內,相同datacenterId和workerId的ID生成器只有一個,做到全局單例.

主要是通過redis原子鎖的來實現的.詳情可看上面的流程圖,主要分為本地ID生成跨實例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,以及如何做到高可用的設計和思路,環境大家提出意見和建議

代碼倉庫 (博客配套代碼)


想獲得最快更新,請關注公眾號

想獲得最快更新,請關注公眾號


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM