做者丨riemann_,原文地址:https://dwz.cn/3IsTKCaNjava
系統惟一ID是咱們在設計一個系統的時候經常會碰見的問題,也經常爲這個問題而糾結。生成ID的方法有不少,適應不一樣的場景、需求以及性能要求。因此有些比較複雜的系統會有多個ID生成的策略。下面就介紹一些常見的ID生成策略。
node
1.數據庫自增加序列或字段
最多見的方式。利用數據庫,全數據庫惟一。git
優勢:github
1)簡單,代碼方便,性能能夠接受。web
2)數字ID自然排序,對分頁或者須要排序的結果頗有幫助。面試
缺點:redis
1)不一樣數據庫語法和實現不一樣,數據庫遷移的時候或多數據庫版本支持的時候須要處理。算法
2)在單個數據庫或讀寫分離或一主多從的狀況下,只有一個主庫能夠生成。有單點故障的風險。數據庫
3)在性能達不到要求的狀況下,比較難於擴展。緩存
4)若是碰見多個系統須要合併或者涉及到數據遷移會至關痛苦。
5)分表分庫的時候會有麻煩。
優化方案:
1)針對主庫單點,若是有多個Master庫,則每一個Master庫設置的起始數字不同,步長同樣,能夠是Master的個數。好比:Master1 生成的是 1,4,7,10,Master2生成的是2,5,8,11 Master3生成的是 3,6,9,12。這樣就能夠有效生成集羣中的惟一ID,也能夠大大下降ID生成數據庫操做的負載。
2. UUID
常見的方式。能夠利用數據庫也能夠利用程序生成,通常來講全球惟一。
優勢:
1)簡單,代碼方便。
2)生成ID性能很是好,基本不會有性能問題。
3)全球惟一,在碰見數據遷移,系統數據合併,或者數據庫變動等狀況下,能夠從容應對。
缺點:
1)沒有排序,沒法保證趨勢遞增。
2)UUID每每是使用字符串存儲,查詢的效率比較低。
3)存儲空間比較大,若是是海量數據庫,就須要考慮存儲量的問題。
4)傳輸數據量大
5)不可讀。
3. UUID的變種
1)爲了解決UUID不可讀,可使用UUID to Int64的方法。
/// <summary>/// 根據GUID獲取惟一數字序列/// </summary>public static long GuidToInt64(){ byte[] bytes = Guid.NewGuid().ToByteArray(); return BitConverter.ToInt64(bytes, 0);}
2)爲了解決UUID無序的問題,NHibernate在其主鍵生成方式中提供了Comb算法(combined guid/timestamp)。保留GUID的10個字節,用另6個字節表示GUID生成的時間(DateTime)。
/// <summary>/// Generate a new <see cref="Guid"/> using the comb algorithm./// </summary>private Guid GenerateComb(){ byte[] guidArray = Guid.NewGuid().ToByteArray(); DateTime baseDate = new DateTime(1900, 1, 1); DateTime now = DateTime.Now; // Get the days and milliseconds which will be used to build //the byte string TimeSpan days = new TimeSpan(now.Ticks - baseDate.Ticks); TimeSpan msecs = now.TimeOfDay; // Convert to a byte array // Note that SQL Server is accurate to 1/300th of a // millisecond so we divide by 3.333333 byte[] daysArray = BitConverter.GetBytes(days.Days); byte[] msecsArray = BitConverter.GetBytes((long) (msecs.TotalMilliseconds / 3.333333)); // Reverse the bytes to match SQL Servers ordering Array.Reverse(daysArray); Array.Reverse(msecsArray); // Copy the bytes into the guid Array.Copy(daysArray, daysArray.Length - 2, guidArray, guidArray.Length - 6, 2); Array.Copy(msecsArray, msecsArray.Length - 4, guidArray, guidArray.Length - 4, 4); return new Guid(guidArray);}
用上面的算法測試一下,獲得以下的結果:做爲比較,前面3個是使用COMB算法得出的結果,最後12個字符串是時間序(統一毫秒生成的3個UUID),過段時間若是再次生成,則12個字符串會比圖示的要大。後面3個是直接生成的GUID。
若是想把時間序放在前面,能夠生成後改變12個字符串的位置,也能夠修改算法類的最後兩個Array.Copy。
4. 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)須要編碼和配置的工做量比較大。
5.Twitter的snowflake算法
snowflake是Twitter開源的分佈式ID生成算法,結果是一個long型的ID。其核心思想是:使用41bit做爲毫秒數,10bit做爲機器的ID(5個bit是數據中心,5個bit的機器ID),12bit做爲毫秒內的流水號(意味着每一個節點在每毫秒能夠產生 4096 個 ID),最後還有一個符號位,永遠是0。
/** * Twitter_Snowflake<br> * SnowFlake的結構以下(每部分用-分開):<br> * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 <br> * 1位標識,因爲long基本類型在Java中是帶符號的,最高位是符號位,正數是0,負數是1,因此id通常是正數,最高位是0<br> * 41位時間截(毫秒級),注意,41位時間截不是存儲當前時間的時間截,而是存儲時間截的差值(當前時間截 - 開始時間截) * 獲得的值),這裏的的開始時間截,通常是咱們的id生成器開始使用的時間,由咱們程序來指定的(以下下面程序IdWorker類的startTime屬性)。41位的時間截,可使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69<br> * 10位的數據機器位,能夠部署在1024個節點,包括5位datacenterId和5位workerId<br> * 12位序列,毫秒內的計數,12位的計數順序號支持每一個節點每毫秒(同一機器,同一時間截)產生4096個ID序號<br> * 加起來恰好64位,爲一個Long型。<br> * SnowFlake的優勢是,總體上按照時間自增排序,而且整個分佈式系統內不會產生ID碰撞(由數據中心ID和機器ID做區分),而且效率較高,經測試,SnowFlake每秒可以產生26萬ID左右。 */public class SnowflakeIdWorker { // ==============================Fields=========================================== /** 開始時間截 (2015-01-01) */ private final long twepoch = 1420041600000L; /** 機器id所佔的位數 */ private final long workerIdBits = 5L; /** 數據標識id所佔的位數 */ private final long datacenterIdBits = 5L; /** 支持的最大機器id,結果是31 (這個移位算法能夠很快的計算出幾位二進制數所能表示的最大十進制數) */ private final long maxWorkerId = -1L ^ (-1L << workerIdBits); /** 支持的最大數據標識id,結果是31 */ private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); /** 序列在id中佔的位數 */ private final long sequenceBits = 12L; /** 機器ID向左移12位 */ private final long workerIdShift = sequenceBits; /** 數據標識id向左移17位(12+5) */ private final long datacenterIdShift = sequenceBits + workerIdBits; /** 時間截向左移22位(5+5+12) */ private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits; /** 生成序列的掩碼,這裏爲4095 (0b111111111111=0xfff=4095) */ private final long sequenceMask = -1L ^ (-1L << sequenceBits); /** 工做機器ID(0~31) */ private long workerId; /** 數據中心ID(0~31) */ private long datacenterId; /** 毫秒內序列(0~4095) */ private long sequence = 0L; /** 上次生成ID的時間截 */ private long lastTimestamp = -1L; //==============================Constructors===================================== /** * 構造函數 * @param workerId 工做ID (0~31) * @param datacenterId 數據中心ID (0~31) */ public SnowflakeIdWorker(long workerId, long datacenterId) { if (workerId > maxWorkerId || workerId < 0) { throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId)); } if (datacenterId > maxDatacenterId || datacenterId < 0) { throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId)); } this.workerId = workerId; this.datacenterId = datacenterId; } // ==============================Methods========================================== /** * 得到下一個ID (該方法是線程安全的) * @return SnowflakeId */ public synchronized long nextId() { long timestamp = timeGen(); //若是當前時間小於上一次ID生成的時間戳,說明系統時鐘回退過這個時候應當拋出異常 if (timestamp < lastTimestamp) { throw new RuntimeException( String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp)); } //若是是同一時間生成的,則進行毫秒內序列 if (lastTimestamp == timestamp) { sequence = (sequence + 1) & sequenceMask; //毫秒內序列溢出 if (sequence == 0) { //阻塞到下一個毫秒,得到新的時間戳 timestamp = tilNextMillis(lastTimestamp); } } //時間戳改變,毫秒內序列重置 else { sequence = 0L; } //上次生成ID的時間截 lastTimestamp = timestamp; //移位並經過或運算拼到一塊兒組成64位的ID return ((timestamp - twepoch) << timestampLeftShift) // | (datacenterId << datacenterIdShift) // | (workerId << workerIdShift) // | sequence; } /** * 阻塞到下一個毫秒,直到得到新的時間戳 * @param lastTimestamp 上次生成ID的時間截 * @return 當前時間戳 */ protected long tilNextMillis(long lastTimestamp) { long timestamp = timeGen(); while (timestamp <= lastTimestamp) { timestamp = timeGen(); } return timestamp; } /** * 返回以毫秒爲單位的當前時間 * @return 當前時間(毫秒) */ protected long timeGen() { return System.currentTimeMillis(); } //==============================Test============================================= /** 測試 */ public static void main(String[] args) { SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 0); for (int i = 0; i < 1000; i++) { long id = idWorker.nextId(); System.out.println(Long.toBinaryString(id)); System.out.println(id); } }}
snowflake算法能夠根據自身項目的須要進行必定的修改。好比估算將來的數據中心個數,每一個數據中心的機器數以及統一毫秒能夠能的併發數來調整在算法中所須要的bit數。
優勢:
1)不依賴於數據庫,靈活方便,且性能優於數據庫。
2)ID按照時間在單機上是遞增的。
缺點:
1)在單機上是遞增的,可是因爲涉及到分佈式環境,每臺機器上的時鐘不可能徹底同步,也許有時候也會出現不是全局遞增的狀況。
6.利用zookeeper生成惟一ID
zookeeper主要經過其znode數據版原本生成序列號,能夠生成32位和64位的數據版本號,客戶端可使用這個版本號來做爲惟一的序列號。不多會使用zookeeper來生成惟一ID。主要是因爲須要依賴zookeeper,而且是多步調用API,若是在競爭較大的狀況下,須要考慮使用分佈式鎖。所以,性能在高併發的分佈式環境下,也不甚理想。
7.MongoDB的ObjectId
MongoDB的ObjectId和snowflake算法相似。它設計成輕量型的,不一樣的機器都能用全局惟一的同種方法方便地生成它。MongoDB 從一開始就設計用來做爲分佈式數據庫,處理多個節點是一個核心要求。使其在分片環境中要容易生成得多。其格式以下:
前4 個字節是從標準紀元開始的時間戳,單位爲秒。時間戳,與隨後的5 個字節組合起來,提供了秒級別的惟一性。因爲時間戳在前,這意味着ObjectId 大體會按照插入的順序排列。這對於某些方面頗有用,如將其做爲索引提升效率。這4 個字節也隱含了文檔建立的時間。絕大多數客戶端類庫都會公開一個方法從ObjectId 獲取這個信息。接下來的3 字節是所在主機的惟一標識符。一般是機器主機名的散列值。這樣就能夠確保不一樣主機生成不一樣的ObjectId,不產生衝突。爲了確保在同一臺機器上併發的多個進程產生的ObjectId 是惟一的,接下來的兩字節來自產生ObjectId 的進程標識符(PID)。前9 字節保證了同一秒鐘不一樣機器不一樣進程產生的ObjectId 是惟一的。後3 字節就是一個自動增長的計數器,確保相同進程同一秒產生的ObjectId 也是不同的。同一秒鐘最多容許每一個進程擁有2563(16 777 216)個不一樣的ObjectId。
實現的源碼能夠到MongoDB官方網站下載。
8.美團點評分佈式ID生成系統
Leaf 最先期需求是各個業務線的訂單ID生成需求。在美團早期,有的業務直接經過DB自增的方式生成ID,有的業務經過redis緩存來生成ID,也有的業務直接用UUID這種方式來生成ID。以上的方式各自有各自的問題,所以咱們決定實現一套分佈式ID生成服務來知足需求。具體Leaf 設計文檔見: leaf 美團分佈式ID生成服務
github地址:
https://github.com/Meituan-Dianping/Leaf
推薦閱讀
本文分享自微信公衆號 - 愛編碼(ilovecode)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。