一篇搞定,分布式系统中唯一主键生成
簡介:?分布式系統中最關鍵的一個問題,ID生成,本文,一篇帶你掌握
系統唯一ID是我們在設計一個系統的時候常常會遇見的問題,也常常為這個問題而糾結。生成ID的方法有很多,適應不同的場景、需求以及性能要求。所以有些比較復雜的系統會有多個ID生成的策略。下面就介紹一些常見的ID生成策略。
最常見的方式。利用數據庫,全數據庫唯一。
優點:
1)簡單,代碼方便,性能可以接受。
2)數字ID天然排序,對分頁或者需要排序的結果很有幫助。
缺點:
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生成數據庫操作的負載。
常見的方式。可以利用數據庫也可以利用程序生成,一般來說全球唯一。
優點:
1)簡單,代碼方便。
2)生成ID性能非常好,基本不會有性能問題。
3)全球唯一,在遇見數據遷移,系統數據合并,或者數據庫變更等情況下,可以從容應對。
缺點:
1)沒有排序,無法保證趨勢遞增。
2)UUID往往是使用字符串存儲,查詢的效率比較低。
3)存儲空間比較大,如果是海量數據庫,就需要考慮存儲量的問題。
4)傳輸數據量大
5)不可讀。
1)為了解決UUID不可讀,可以使用UUID to Int64的方法。及
///
/// 根據GUID獲取唯一數字序列
///
public static long GuidToInt64()
{
}
2)為了解決UUID無序的問題,NHibernate在其主鍵生成方式中提供了Comb算法(combined guid/timestamp)。保留GUID的10個字節,用另6個字節表示GUID生成的時間(DateTime)。
///
/// Generate a new using the comb algorithm.
///
private Guid GenerateComb()
{
}
用上面的算法測試一下,得到如下的結果:作為比較,前面3個是使用COMB算法得出的結果,最后12個字符串是時間序(統一毫秒生成的3個UUID),過段時間如果再次生成,則12個字符串會比圖示的要大。后面3個是直接生成的GUID。
如果想把時間序放在前面,可以生成后改變12個字符串的位置,也可以修改算法類的最后兩個Array.Copy。
當使用數據庫來生成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)需要編碼和配置的工作量比較大。
snowflake是Twitter開源的分布式ID生成算法,結果是一個long型的ID。其核心思想是:使用41bit作為毫秒數,10bit作為機器的ID(5個bit是數據中心,5個bit的機器ID),12bit作為毫秒內的流水號(意味著每個節點在每毫秒可以產生 4096 個 ID),最后還有一個符號位,永遠是0。具體實現的代碼可以參看https://github.com/twitter/snowflake。
C#代碼如下:
///
/// From: https://github.com/twitter/snowflake /// An object that generates IDs. /// This is broken into a separate class in case /// we ever want to support multiple worker threads /// per process /// </summary> public class IdWorker {private long workerId;private long datacenterId;private long sequence = 0L;private static long twepoch = 1288834974657L;private static long workerIdBits = 5L;private static long datacenterIdBits = 5L;private static long maxWorkerId = -1L ^ (-1L << (int)workerIdBits);private static long maxDatacenterId = -1L ^ (-1L << (int)datacenterIdBits);private static long sequenceBits = 12L;private long workerIdShift = sequenceBits;private long datacenterIdShift = sequenceBits + workerIdBits;private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;private long sequenceMask = -1L ^ (-1L << (int)sequenceBits);private long lastTimestamp = -1L;private static object syncRoot = new object();public IdWorker(long workerId, long datacenterId){// sanity check for workerIdif (workerId > maxWorkerId || workerId < 0){throw new ArgumentException(string.Format("worker Id can't be greater than %d or less than 0", maxWorkerId));}if (datacenterId > maxDatacenterId || datacenterId < 0){throw new ArgumentException(string.Format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));}this.workerId = workerId;this.datacenterId = datacenterId;}public long nextId(){lock (syncRoot){long timestamp = timeGen();if (timestamp < lastTimestamp){throw new ApplicationException(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;}lastTimestamp = timestamp;return ((timestamp - twepoch) << (int)timestampLeftShift) | (datacenterId << (int)datacenterIdShift) | (workerId << (int)workerIdShift) | sequence;}}protected long tilNextMillis(long lastTimestamp){long timestamp = timeGen();while (timestamp <= lastTimestamp){timestamp = timeGen();}return timestamp;}protected long timeGen(){return (long)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds;} }測試代碼如下:
private static void TestIdWorker()
{HashSet<long> set = new HashSet<long>();IdWorker idWorker1 = new IdWorker(0, 0);IdWorker idWorker2 = new IdWorker(1, 0);Thread t1 = new Thread(() => DoTestIdWoker(idWorker1, set));Thread t2 = new Thread(() => DoTestIdWoker(idWorker2, set));t1.IsBackground = true;t2.IsBackground = true;t1.Start();t2.Start();try{Thread.Sleep(30000);t1.Abort();t2.Abort();}catch (Exception e){}Console.WriteLine("done");}private static void DoTestIdWoker(IdWorker idWorker, HashSet<long> set){while (true){long id = idWorker.nextId();if (!set.Add(id)){Console.WriteLine("duplicate:" + id);}Thread.Sleep(1);}}snowflake算法可以根據自身項目的需要進行一定的修改。比如估算未來的數據中心個數,每個數據中心的機器數以及統一毫秒可以能的并發數來調整在算法中所需要的bit數。
優點:
1)不依賴于數據庫,靈活方便,且性能優于數據庫。
2)ID按照時間在單機上是遞增的。
缺點:
1)在單機上是遞增的,但是由于涉及到分布式環境,每臺機器上的時鐘不可能完全同步,也許有時候也會出現不是全局遞增的情況。
zookeeper主要通過其znode數據版本來生成序列號,可以生成32位和64位的數據版本號,客戶端可以使用這個版本號來作為唯一的序列號。很少會使用zookeeper來生成唯一ID。主要是由于需要依賴zookeeper,并且是多步調用API,如果在競爭較大的情況下,需要考慮使用分布式鎖。因此,性能在高并發的分布式環境下,也不甚理想。
MongoDB的ObjectId和snowflake算法類似。它設計成輕量型的,不同的機器都能用全局唯一的同種方法方便地生成它。MongoDB 從一開始就設計用來作為分布式數據庫,處理多個節點是一個核心要求。使其在分片環境中要容易生成得多。
其格式如下:
前4 個字節是從標準紀元開始的時間戳,單位為秒。時間戳,與隨后的5 個字節組合起來,提供了秒級別的唯一性。由于時間戳在前,這意味著ObjectId 大致會按照插入的順序排列。這對于某些方面很有用,如將其作為索引提高效率。這4 個字節也隱含了文檔創建的時間。絕大多數客戶端類庫都會公開一個方法從ObjectId 獲取這個信息。接下來的3 字節是所在主機的唯一標識符。通常是機器主機名的散列值。這樣就可以確保不同主機生成不同的ObjectId,不產生沖突。為了確保在同一臺機器上并發的多個進程產生的ObjectId 是唯一的,接下來的兩字節來自產生ObjectId 的進程標識符(PID)。前9 字節保證了同一秒鐘不同機器不同進程產生的ObjectId 是唯一的。后3 字節就是一個自動增加的計數器,確保相同進程同一秒產生的ObjectId 也是不一樣的。同一秒鐘最多允許每個進程擁有2563(16 777 216)個不同的ObjectId。
實現的源碼可以到MongoDB官方網站下載。
總結
以上是生活随笔為你收集整理的一篇搞定,分布式系统中唯一主键生成的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 优酷播控实践:基于规则引擎的投放管控模型
- 下一篇: 揭秘 SIGCOMM 20‘ 论文:阿里