不少人一想到IM應用開發,第一印象就是「長鏈接」、「socket」、「保活」、「協議」這些關鍵詞,沒錯,這些確實是IM開發中確定會涉及的技術範疇。html
但,當你真正開始編寫第一行代碼時,最現實的問題其實是「聊天消息ID該怎麼生成?」這個看似微不足道的小事情。說它看似微不足道,是由於在IM裏它太日常了,到處可見它的身影。不過,雖然看似微不足道,但實際卻很重要,由於它的生成算法和生成策略的優劣在某種意義上來講,決定了你的IM應用層某些功能實現的難易度。node
有籤於此,即時通信網專門整理了「IM消息ID技術專題」系列文章,但願能帶給你對這個看似微小但卻很重要的技術點有更深入的理解和最佳實踐思路。git
本文是專題系列文章的第5篇,專門介紹百度開源的分佈式消息ID生成器UidGenerator的算法邏輯、實現思路、重點源碼解讀等,或許能帶給你更多的啓發。github
全局ID(常見的好比:IM聊天系統中的消息ID、電商系統中的訂單號、外賣應用中的訂單號等)服務是分佈式服務中的基礎服務,須要保持全局惟1、高效、高可靠性。有些時候還可能要求保持單調,但也並不是必定要嚴格遞增或者遞減。算法
全局ID也能夠經過數據庫的自增主鍵來獲取,可是若是要求QPS很高顯然是不現實的。sql
UidGenerator(備用地址)工程是百度開源的基於Snowflake算法的惟一ID生成器(百度對Snowflake算法進行了改進),引入了高性能隊列高性能隊列disruptor中RingBuffer思想,進一步提高了效率。docker
UidGenerator是Java語言實現的,它以組件形式工做在應用項目中,支持自定義workerId位數和初始化策略,,從而適用於docker等虛擬化環境下實例自動重啓、漂移等場景。 數據庫
在技術實現上,UidGenerator有如下關鍵特性:編程
1)UidGenerator經過借用將來時間來解決sequence自然存在的併發限制;數組
2)採用RingBuffer來緩存已生成的UID, 並行化UID的生產和消費;
3)同時對CacheLine補齊,避免了由RingBuffer帶來的硬件級「僞共享」問題。
基於以上技術特性,UidGenerator的單機壓力測試數據顯示,其QPS可高達600萬。
依賴的環境:
1)Java8及以上版本(代碼中使用了函數式編程語句等新特性,請見:uid-generator源碼在線版);
2)MySQL(內置WorkerID分配器, 啓動階段經過DB進行分配; 如自定義實現, 則DB非必選依賴)。
如下是UidGenerator工程的相關資源:
1)完整源碼地址:https://github.com/baidu/uid-generator
2)備用源碼地址:https://github.com/52im/uid-generator
3)源碼在線閱讀:http://docs.52im.net/extend/docs/src/uid-generator/(推薦)
友情提示:本節文字內容摘選自《IM消息ID技術專題(四):深度解密美團的分佈式ID生成算法》一文,若是您想了解美團對於SnowFlake算法的理解和應用狀況,可詳細閱讀之。
SnowFlake 算法,是 Twitter 開源的分佈式 ID 生成算法。其核心思想就是:使用一個 64 bit 的 long 型的數字做爲全局惟一 ID。
這 64 個 bit 中,其中 1 個 bit 是不用的,而後用其中的 41 bit 做爲毫秒數,用 10 bit 做爲工做機器 ID,12 bit 做爲序列號。
SnowFlake的ID構成:
(本圖引用自《IM消息ID技術專題(四):深度解密美團的分佈式ID生成算法》)
SnowFlake的ID樣本:
(本圖引用自《IM消息ID技術專題(四):深度解密美團的分佈式ID生成算法》)
給你們舉個例子吧,如上圖所示,好比下面那個 64 bit 的 long 型數字:
1)第一個部分,是 1 個 bit:0,這個是無心義的;
2)第二個部分,是 41 個 bit:表示的是時間戳;
3)第三個部分,是 5 個 bit:表示的是機房 ID,10001;
4)第四個部分,是 5 個 bit:表示的是機器 ID,1 1001;
5)第五個部分,是 12 個 bit:表示的序號,就是某個機房某臺機器上這一毫秒內同時生成的 ID 的序號,0000 00000000。
① 1 bit:是不用的,爲啥呢?
由於二進制裏第一個 bit 爲若是是 1,那麼都是負數,可是咱們生成的 ID 都是正數,因此第一個 bit 統一都是 0。
② 41 bit:表示的是時間戳,單位是毫秒。
41 bit 能夠表示的數字多達 2^41 - 1,也就是能夠標識 2 ^ 41 - 1 個毫秒值,換算成年就是表示 69 年的時間。
③ 10 bit:記錄工做機器 ID,表明的是這個服務最多能夠部署在 2^10 臺機器上,也就是 1024 臺機器。
可是 10 bit 裏 5 個 bit 表明機房 id,5 個 bit 表明機器 ID。意思就是最多表明 2 ^ 5 個機房(32 個機房),每一個機房裏能夠表明 2 ^ 5 個機器(32 臺機器)。
④12 bit:這個是用來記錄同一個毫秒內產生的不一樣 ID。
12 bit 能夠表明的最大正整數是 2 ^ 12 - 1 = 4096,也就是說能夠用這個 12 bit 表明的數字來區分同一個毫秒內的 4096 個不一樣的 ID。理論上snowflake方案的QPS約爲409.6w/s,這種分配方式能夠保證在任何一個IDC的任何一臺機器在任意毫秒內生成的ID都是不一樣的。
簡單來講,你的某個服務假設要生成一個全局惟一 ID,那麼就能夠發送一個請求給部署了 SnowFlake 算法的系統,由這個 SnowFlake 算法系統來生成惟一 ID。
最終一個 64 個 bit 的 ID 就出來了,相似於:
(本圖引用自《IM消息ID技術專題(四):深度解密美團的分佈式ID生成算法》)
這個算法能夠保證說,一個機房的一臺機器上,在同一毫秒內,生成了一個惟一的 ID。可能一個毫秒內會生成多個 ID,可是有最後 12 個 bit 的序號來區分開來。
下面咱們簡單看看這個 SnowFlake 算法的一個代碼實現,這就是個示例,你們若是理解了這個意思以後,之後能夠本身嘗試改造這個算法。
總之就是用一個 64 bit 的數字中各個 bit 位來設置不一樣的標誌位,區分每個 ID。
SnowFlake 算法的一個典型Java實現代碼,能夠參見文章中的第「6.5 方案四:SnowFlake 算法的思想分析」節:《通俗易懂:如何設計能支撐百萬併發的數據庫架構?》,是Jack Jiang曾在某項目中實際使用過的代碼。
對於份布式的業務系統來講,SnowFlake算法的優缺點以下。
► 優勢:
1)毫秒數在高位,自增序列在低位,整個ID都是趨勢遞增的;
2)不依賴數據庫等第三方系統,以服務的方式部署,穩定性更高,生成ID的性能也是很是高的;
3)能夠根據自身業務特性分配bit位,很是靈活。
► 缺點:
強依賴機器時鐘,若是機器上時鐘回撥,會致使發號重複或者服務會處於不可用狀態。
經過上節,咱們知道了原版SnowFlake算法的基本構成。
具體是,原版SnowFlake算法核心組成:
原版SnowFlake算法各字段的具體意義是:
1)1位sign標識位;
2)41位時間戳;
3)10位workId(即5位數據中心id+5位工做機器id);
4)12位自增序列。
而UidGenerator改進後的SnowFlake算法核心組成以下圖:
簡單來講,UidGenerator能保證「指定機器 & 同一時刻 & 某一併發序列」,是惟一,並據今生成一個64 bits的惟一ID(long),且默認採用上圖字節分配方式。
與原版的snowflake算法不一樣,UidGenerator還支持自定義時間戳、工做機器id和序列號等各部分的位數,以應用於不一樣場景(詳見源碼實現)。
如上圖所示,UidGenerator默認ID中各數據位的含義以下:
經過閱讀UidGenerator的源碼可知,UidGenerator的具體實現有兩種選擇,即 DefaultUidGenerator 和 CachedUidGenerator。咱們分別來看看這兩個具體代碼實現的精妙之處。
DefaultUidGenerator 的源碼很清楚的說明了幾個生成ID的關鍵位的實現邏輯。
1)delta seconds(28 bits):
這個值是指當前時間與epoch時間的時間差,且單位爲秒。epoch時間就是指集成DefaultUidGenerator生成分佈式ID服務第一次上線的時間,可配置,也必定要根據你的上線時間進行配置,由於默認的epoch時間但是2016-09-20,不配置的話,會浪費好幾年的可用時間。
2)worker id(22bits):
接下來講一下DefaultUidGenerator是如何給worker id賦值的,搭建DefaultUidGenerator的話,須要建立一個表:
DROP DATABASE IF EXISTS `xxxx`; CREATE DATABASE `xxxx` ; use `xxxx`; DROP TABLE IF EXISTS WORKER_NODE; CREATE TABLE WORKER_NODE ( ID BIGINT NOT NULL AUTO_INCREMENT COMMENT 'auto increment id', HOST_NAME VARCHAR(64) NOT NULL COMMENT 'host name', PORT VARCHAR(64) NOT NULL COMMENT 'port', TYPE INT NOT NULL COMMENT 'node type: ACTUAL or CONTAINER', LAUNCH_DATE DATE NOT NULL COMMENT 'launch date', MODIFIED TIMESTAMP NOT NULL COMMENT 'modified time', CREATED TIMESTAMP NOT NULL COMMENT 'created time', PRIMARY KEY(ID) ) COMMENT='DB WorkerID Assigner for UID Generator', ENGINE = INNODB;
DefaultUidGenerator會在集成用它生成分佈式ID的實例啓動的時候,往這個表中插入一行數據,獲得的id值就是準備賦給workerId的值。因爲workerId默認22位,那麼,集成DefaultUidGenerator生成分佈式ID的全部實例重啓次數是不容許超過4194303次(即2^22-1),不然會拋出異常。
3)sequence(13bits):
核心代碼以下,幾個實現的關鍵點:
a. synchronized保證線程安全;
b. 若是時間有任何的回撥,那麼直接拋出異常;
c. 若是當前時間和上一次是同一秒時間,那麼sequence自增。若是同一秒內自增值超過2^13-1,那麼就會自旋等待下一秒(getNextSecond);
d. 若是是新的一秒,那麼sequence從新從0開始。
(上述源碼節選自:DefaultUidGenerator 類中的 nextId() 方法)
4)小結:
經過DefaultUidGenerator的實現可知,它對時鐘回撥的處理比較簡單粗暴。另外若是使用UidGenerator的DefaultUidGenerator方式生成分佈式ID,必定要根據你的業務的狀況和特色,調整各個字段佔用的位數:
<!-- Specified bits & epoch as your demand. No specified the default value will be used --> <property name="timeBits" value="29"/> <property name="workerBits" value="21"/> <property name="seqBits" value="13"/> <property name="epochStr" value="2016-09-20"/>
CachedUidGenerator是DefaultUidGenerator的重要改進實現。它的核心利用了RingBuffer,它本質上是一個數組,數組中每一個項被稱爲slot。CachedUidGenerator設計了兩個RingBuffer,一個保存惟一ID,一個保存flag。RingBuffer的尺寸是2^n,n必須是正整數。
如下是CachedUidGenerator中的RingBuffer原理示意圖:
擴展知識:什麼是RingBuffer?
Ring Buffer的概念,其實來自於Linux內核(Maybe),是爲解決某些特殊狀況下的競爭問題提供了一種免鎖的方法。這種特殊的狀況就是當生產者和消費者都只有一個,而在其它狀況下使用它也是必需要加鎖的。
環形緩衝區一般有一個讀指針和一個寫指針。讀指針指向環形緩衝區中可讀的數據,寫指針指向環形緩衝區中可寫的緩衝區。經過移動讀指針和寫指針就能夠實現緩衝區的數據讀取和寫入。在一般狀況下,環形緩衝區的讀用戶僅僅會影響讀指針,而寫用戶僅僅會影響寫指針。若是僅僅有一個讀用戶和一個寫用戶,那麼不須要添加互斥保護機制就能夠保證數據的正確性。若是有多個讀寫用戶訪問環形緩衝區,那麼必須添加互斥保護機制來確保多個用戶互斥訪問環形緩衝區。
更多具體的 CachedUidGenerator 的代碼實現,有興趣能夠仔細讀一讀,也能夠前往百度uid-generator工程的說明頁看看具體的算法原理,這裏就再也不贅述。
簡要的小結一下,CachedUidGenerator方式主要經過採起以下一些措施和方案規避了時鐘回撥問題和加強惟一性:
CachedUidGenerator經過緩存的方式預先生成一批惟一ID列表,能夠解決惟一ID獲取時候的耗時。但這種方式也有很差點,一方面須要耗費內存來緩存這部分數據,另外若是訪問量不大的狀況下,提早生成的UID中的時間戳多是很早以前的。而對於大部分的場景來講,DefaultUidGenerator 就能夠知足相關的需求了,不必來湊CachedUidGenerator這個熱鬧。
另外,關於UidGenerator比特位分配的建議:
對於併發數要求不高、指望長期使用的應用, 可增長timeBits位數, 減小seqBits位數. 例如節點採起用完即棄的WorkerIdAssigner策略, 重啓頻率爲12次/天, 那麼配置成{"workerBits":23,"timeBits":31,"seqBits":9}時, 可支持28個節點以總體併發量14400 UID/s的速度持續運行68年.
對於節點重啓頻率頻繁、指望長期使用的應用, 可增長workerBits和timeBits位數, 減小seqBits位數. 例如節點採起用完即棄的WorkerIdAssigner策略, 重啓頻率爲24*12次/天, 那麼配置成{"workerBits":27,"timeBits":30,"seqBits":6}時, 可支持37個節點以總體併發量2400 UID/s的速度持續運行34年.
UidGenerator的測試數據顯示,在MacBook Pro(2.7GHz Intel Core i5, 8G DDR3)上進行的CachedUidGenerator(單實例)的UID吞吐量測試狀況以下。
首先:固定住workerBits爲任選一個值(如20), 分別統計timeBits變化時(如從25至32, 總時長分別對應1年和136年)的吞吐量, 測試結果以下圖所示:
再固定住timeBits爲任選一個值(如31), 分別統計workerBits變化時(如從20至29, 總重啓次數分別對應1百萬和500百萬)的吞吐量, 測試結果以下圖所示:
因而可知:無論如何配置, CachedUidGenerator總能提供600萬/s的穩定吞吐量,只是使用年限會有所減小,這真的是太棒了!
最後:固定住workerBits和timeBits位數(如23和31), 分別統計不一樣數目(如1至8,本機CPU核數爲4)的UID使用者狀況下的吞吐量,測試結果以下圖所示:
[1] 改進版Snowflake全局ID生成器-uid-generator
[2] UidGenerator