1. 程式人生 > >分散式-全域性唯一id

分散式-全域性唯一id

  • 系統唯一ID是我們在設計一個系統的時候常常會遇見的問題,也常常為這個問題而糾結。
  • 生成ID的方法有很多,適應不同的場景、需求以及效能要求。所以有些比較複雜的系統會有多個ID生成的策略。

一、常見的ID生成策略。

1.1 資料庫自增長序列或欄位

  • 最常見的方式。利用資料庫,全資料庫唯一。

1.1.1 優點:

  • 1)簡單,程式碼方便,效能可以接受。
  • 2)數字ID天然排序,對分頁或者需要排序的結果很有幫助。

1.1.2 缺點:

  • 1)不同資料庫語法和實現不同,資料庫遷移的時候或多資料庫版本支援的時候需要處理。
  • 2)在單個數據庫或讀寫分離或一主多從的情況下,只有一個主庫可以生成。有單點故障的風險。
  • 3)在效能達不到要求的情況下,比較難於擴充套件。
  • 4)如果遇見多個系統需要合併或者涉及到資料遷移會相當痛苦。
  • 5)分表分庫的時候會有麻煩。

1.1.3 優化方案:

  • 1)針對主庫單點,如果有多個Master庫,則每個Master庫設定的起始數字不一樣,步長一樣,可以是Master的個數。
    比如:Master1 生成的是 1,4,7,10,Master2生成的是2,5,8,11 Master3生成的是 3,6,9,12。這樣就可以有效生成叢集中的唯一ID,也可以大大降低ID生成資料庫操作的負載。

1.2 UUID

  • 常見的方式。可以利用資料庫也可以利用程式生成,一般來說全球唯一。

1.2.1優點:

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

1.2.2 缺點:

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

1.3 UUID的變種

  • 1)為了解決UUID不可讀,可以使用UUID to Int64的方法。及
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。

1.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

C3,8,13,18,23

D4,9,14,19,24

E5,10,15,20,25
  • 這個,隨便負載到哪個機確定好,未來很難做修改。
    但是3-5臺伺服器基本能夠滿足器上,都可以獲得不同的ID。但是步長和初始值一定需要事先需要了。使用Redis叢集也可以方式單點故障的問題。
  • 另外,比較適合使用Redis來生成每天從0開始的流水號。
    比如訂單號=日期+當日自增長號。可以每天在Redis中生成一個Key,使用INCR進行累加。

1.4.1 優點:

  • 1)不依賴於資料庫,靈活方便,且效能優於資料庫。
  • 2)數字ID天然排序,對分頁或者需要排序的結果很有幫助。

1.4.2 缺點:

  • 1)如果系統中沒有Redis,還需要引入新的元件,增加系統複雜度。
  • 2)需要編碼和配置的工作量比較大。

1.5 Twitter的snowflake演算法

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

1.5.1 概述

分散式系統中,有一些需要使用全域性唯一ID的場景,這種時候為了防止ID衝突可以使用36位的UUID,但是UUID有一些缺點,首先他相對比較長,另外UUID一般是無序的。
有些時候我們希望能使用一種簡單一些的ID,並且希望ID能夠按照時間有序生成。
而twitter的snowflake解決了這種需求,最初Twitter把儲存系統從MySQL遷移到Cassandra,因為Cassandra沒有順序ID生成機制,所以開發了這樣一套全域性唯一ID生成服務。

1.5.2 結構

snowflake的結構如下(每部分用-分開):

0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000

第一位為未使用,接下來的41位為毫秒級時間(41位的長度可以使用69年),然後是5位datacenterId和5位workerId(10位的長度最多支援部署1024個節點) ,最後12位是毫秒內的計數(12位的計數順序號支援每個節點每毫秒產生4096個ID序號)

一共加起來剛好64位,為一個Long型。(轉換成字串後長度最多19)

snowflake生成的ID整體上按照時間自增排序,並且整個分散式系統內不會產生ID碰撞(由datacenter和workerId作區分),並且效率較高。經測試snowflake每秒能夠產生26萬個ID。

1.5.3 原始碼

/**
 * 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);
        }
    }
}

1.6 利用zookeeper生成唯一ID

  • zookeeper主要通過其znode資料版本來生成序列號
    可以生成32位和64位的資料版本號,客戶端可以使用這個版本號來作為唯一的序列號。

  • 很少會使用zookeeper來生成唯一ID。
    主要是由於需要依賴zookeeper,並且是多步呼叫API,如果在競爭較大的情況下,需要考慮使用分散式鎖。因此,效能在高併發的分散式環境下,也不甚理想。

1.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官方網站下載。