一篇搞定,分佈式系統中惟一主鍵生成

簡介: 分佈式系統中最關鍵的一個問題,ID生成,本文,一篇帶你掌握node

系統惟一ID是咱們在設計一個系統的時候經常會碰見的問題,也經常爲這個問題而糾結。生成ID的方法有不少,適應不一樣的場景、需求以及性能要求。因此有些比較複雜的系統會有多個ID生成的策略。下面就介紹一些常見的ID生成策略。git

  1. 數據庫自增加序列或字段
    最多見的方式。利用數據庫,全數據庫惟一。

優勢:github

1)簡單,代碼方便,性能能夠接受。算法

2)數字ID自然排序,對分頁或者須要排序的結果頗有幫助。數據庫

缺點:服務器

1)不一樣數據庫語法和實現不一樣,數據庫遷移的時候或多數據庫版本支持的時候須要處理。併發

2)在單個數據庫或讀寫分離或一主多從的狀況下,只有一個主庫能夠生成。有單點故障的風險。less

3)在性能達不到要求的狀況下,比較難於擴展。分佈式

4)若是碰見多個系統須要合併或者涉及到數據遷移會至關痛苦。ide

5)分表分庫的時候會有麻煩。

優化方案:

1)針對主庫單點,若是有多個Master庫,則每一個Master庫設置的起始數字不同,步長同樣,能夠是Master的個數。好比:Master1 生成的是 1,4,7,10,Master2生成的是2,5,8,11 Master3生成的是 3,6,9,12。這樣就能夠有效生成集羣中的惟一ID,也能夠大大下降ID生成數據庫操做的負載。

  1. UUID
    常見的方式。能夠利用數據庫也能夠利用程序生成,通常來講全球惟一。

優勢:

1)簡單,代碼方便。

2)生成ID性能很是好,基本不會有性能問題。

3)全球惟一,在碰見數據遷移,系統數據合併,或者數據庫變動等狀況下,能夠從容應對。

缺點:

1)沒有排序,沒法保證趨勢遞增。

2)UUID每每是使用字符串存儲,查詢的效率比較低。

3)存儲空間比較大,若是是海量數據庫,就須要考慮存儲量的問題。

4)傳輸數據量大

5)不可讀。

  1. UUID的變種
    1)爲了解決UUID不可讀,可使用UUID to Int64的方法。及

///
/// 根據GUID獲取惟一數字序列
///
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)。

///
/// Generate a new using the comb algorithm.
///
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。
image.png

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

  1. 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)須要編碼和配置的工做量比較大。

  1. Twitter的snowflake算法
    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 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)在單機上是遞增的,可是因爲涉及到分佈式環境,每臺機器上的時鐘不可能徹底同步,也許有時候也會出現不是全局遞增的狀況。

  1. 利用zookeeper生成惟一ID
    zookeeper主要經過其znode數據版原本生成序列號,能夠生成32位和64位的數據版本號,客戶端可使用這個版本號來做爲惟一的序列號。不多會使用zookeeper來生成惟一ID。主要是因爲須要依賴zookeeper,而且是多步調用API,若是在競爭較大的狀況下,須要考慮使用分佈式鎖。所以,性能在高併發的分佈式環境下,也不甚理想。
  2. MongoDB的ObjectId
    MongoDB的ObjectId和snowflake算法相似。它設計成輕量型的,不一樣的機器都能用全局惟一的同種方法方便地生成它。MongoDB 從一開始就設計用來做爲分佈式數據庫,處理多個節點是一個核心要求。使其在分片環境中要容易生成得多。

其格式以下:

image.png

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

實現的源碼能夠到MongoDB官方網站下載。

相關文章
相關標籤/搜索