如何保證分佈式系統中 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生成數據庫操作的負載。

UUID

常見的方式。可以利用數據庫也可以利用程序生成,一般來說全球唯一。

優點:

1)簡單,代碼方便。 2)生成ID性能非常好,基本不會有性能問題。 3)全球唯一,在遇見數據遷移,系統數據合併,或者數據庫變更等情況下,可以從容應對。

缺點:

1)沒有排序,無法保證趨勢遞增; 2)UUID往往是使用字符串存儲,查詢的效率比較低; 3)存儲空間比較大,如果是海量數據庫,就需要考慮存儲量的問題; 4)傳輸數據量大; 5)不可讀。

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。

 

如何保證分佈式系統中 ID 的唯一性

 

 

如果想把時間序放在前面,可以生成後改變 12 個字符串的位置,也可以修改算法類的最後兩個 Array.Copy。

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)需要編碼和配置的工作量比較大。

Twitter的snowflake算法

snowflake 是 Twitter 開源的分佈式 ID 生成算法,結果是一個 long 型的 ID。其核心思想是:使用41bit作爲毫秒數,10 bit 作爲機器的 ID(5個 bit 是數據中心,5個 bit的機器ID),12 bit作爲毫秒內的流水號(意味着每個節點在每毫秒可以產生 4096 個 ID),最後還有一個符號位,永遠是0。具體實現的代碼可以參看https://github.com/twitter/snowflake。

C#代碼如下:

/// <summary>
    /// 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 workerId
            if (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生成唯一ID

zookeeper 主要通過其 znode 數據版本來生成序列號,可以生成 32 位和 64 位的數據版本號,客戶端可以使用這個版本號來作爲唯一的序列號。 很少會使用 zookeeper 來生成唯一 ID。主要是由於需要依賴 zookeeper,並且是多步調用API,如果在競爭較大的情況下,需要考慮使用分佈式鎖。因此,性能在高併發的分佈式環境下,也不甚理想。

MongoDB的ObjectId

MongoDB 的 ObjectId 和 snowflake 算法類似。它設計成輕量型的,不同的機器都能用全局唯一的同種方法方便地生成它。MongoDB 從一開始就設計用來作爲分佈式數據庫,處理多個節點是一個核心要求。使其在分片環境中要容易生成得多。

其格式如下:

 

如何保證分佈式系統中 ID 的唯一性

 

 

前4 個字節是從標準紀元開始的時間戳,單位爲秒。時間戳,與隨後的5 個字節組合起來,提供了秒級別的唯一性。由於時間戳在前,這意味着 ObjectId 大致會按照插入的順序排列。這對於某些方面很有用,如將其作爲索引提高效率。這4 個字節也隱含了文檔創建的時間。絕大多數客戶端類庫都會公開一個方法從ObjectId 獲取這個信息。 接下來的3 字節是所在主機的唯一標識符。通常是機器主機名的散列值。這樣就可以確保不同主機生成不同的ObjectId,不產生衝突。 爲了確保在同一臺機器上併發的多個進程產生的 ObjectId 是唯一的,接下來的兩字節來自產生 ObjectId 的進程標識符(PID)。 前9 字節保證了同一秒鐘不同機器不同進程產生的 ObjectId 是唯一的。後3 字節就是一個自動增加的計數器,確保相同進程同一秒產生的 ObjectId 也是不一樣的。同一秒鐘最多允許每個進程擁有2563(16 777 216)個不同的 ObjectId。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章