分佈式Id生成方案

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

1. 數據庫自增長序列或字段

最常見的方式。利用數據庫,全數據庫唯一。

優點:

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生成數據庫操作的負載。

 

2. UUID

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

優點:

1)簡單,代碼方便。

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

3)全球唯一,在遇見數據遷移,系統數據合併,或者數據庫變更等情況下,可以從容應對。

 

缺點:

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

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

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

4)傳輸數據量大

5)不可讀。

 

3. 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)

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。

ODX}_`4N5X$F93OAS~`8Z)C

如果想把時間序放在前面,可以生成後改變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作爲毫秒數,下圖中1010bit作爲機器的ID(5個bit是數據中心,5個bit的機器ID),12bit作爲毫秒內的流水號(意味着每個節點在每毫秒可以產生 4096 個 ID),最後還有一個符號位,永遠是0。

java代碼如下:

import java.lang.management.ManagementFactory;
import java.net.InetAddress;
import java.net.NetworkInterface;

/**
 * <p>名稱:IdWorker.java</p>
 * <p>描述:分佈式自增長ID</p>
 * <pre>
 *     Twitter的 Snowflake JAVA實現方案
 * </pre>
 * 核心代碼爲其IdWorker這個類實現,其原理結構如下,分別用一個0表示一位,用—分割開部分的作用:
 * 1||0---0000000000 0000000000 0000000000 0000000000 0 --- 00000 ---00000 ---000000000000
 * 在上面的字符串中,第一位爲未使用(實際上也可作爲long的符號位),接下來的41位爲毫秒級時間,
 * 然後5位datacenter標識位,5位機器ID(並不算標識符,實際是爲線程標識),
 * 然後12位該毫秒內的當前毫秒內的計數,加起來剛好64位,爲一個Long型。
 * 這樣的好處是,整體上按照時間自增排序,並且整個分佈式系統內不會產生ID碰撞(由datacenter和機器ID作區分),
 * 並且效率較高,經測試,snowflake每秒能夠產生26萬ID左右,完全滿足需要。
 * <p>
 * 64位ID (42(毫秒)+5(機器ID)+5(業務編碼)+12(重複累加))
 *
 * @author Polim
 */
public class IdWorker {
    // 時間起始標記點,作爲基準,一般取系統的最近時間(一旦確定不能變動)
    private final static long twepoch = 1288834974657L;
    // 機器標識位數
    private final static long workerIdBits = 5L;
    // 數據中心標識位數
    private final static long datacenterIdBits = 5L;
    // 機器ID最大值
    private final static long maxWorkerId = -1L ^ (-1L << workerIdBits);
    // 數據中心ID最大值
    private final static long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
    // 毫秒內自增位
    private final static long sequenceBits = 12L;
    // 機器ID偏左移12位
    private final static long workerIdShift = sequenceBits;
    // 數據中心ID左移17位
    private final static long datacenterIdShift = sequenceBits + workerIdBits;
    // 時間毫秒左移22位
    private final static long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

    private final static long sequenceMask = -1L ^ (-1L << sequenceBits);
    /* 上次生產id時間戳 */
    private static long lastTimestamp = -1L;
    // 0,併發控制
    private long sequence = 0L;

    private final long workerId;
    // 數據標識id部分
    private final long datacenterId;

    public IdWorker(){
        this.datacenterId = getDatacenterId(maxDatacenterId);
        this.workerId = getMaxWorkerId(datacenterId, maxWorkerId);
    }
    /**
     * @param workerId
     *            工作機器ID
     * @param datacenterId
     *           數據中心編號
     */
    public IdWorker(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;
    }
    /**
     * 獲取下一個ID
     *
     * @return
     */
    public synchronized long nextId() {
        long timestamp = timeGen();
        if (timestamp < lastTimestamp) {
            throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
        }

        if (lastTimestamp == timestamp) {
            // 當前毫秒內,則+1
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) {
                // 當前毫秒內計數滿了,則等待下一秒
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0L;
        }
        lastTimestamp = timestamp;
        // ID偏移組合生成最終的ID,並返回ID
        long nextId = ((timestamp - twepoch) << timestampLeftShift)
                | (datacenterId << datacenterIdShift)
                | (workerId << workerIdShift) | sequence;

        return nextId;
    }

    private long tilNextMillis(final long lastTimestamp) {
        long timestamp = this.timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = this.timeGen();
        }
        return timestamp;
    }

    private long timeGen() {
        return System.currentTimeMillis();
    }

    /**
     * <p>
     * 獲取 maxWorkerId
     * </p>
     */
    protected static long getMaxWorkerId(long datacenterId, long maxWorkerId) {
        StringBuffer mpid = new StringBuffer();
        mpid.append(datacenterId);
        String name = ManagementFactory.getRuntimeMXBean().getName();
        if (!name.isEmpty()) {
         /*
          * GET jvmPid
          */
            mpid.append(name.split("@")[0]);
        }
      /*
       * MAC + PID 的 hashcode 獲取16個低位
       */
        return (mpid.toString().hashCode() & 0xffff) % (maxWorkerId + 1);
    }

    /**
     * <p>
     * 數據標識id部分
     * </p>
     */
    protected static long getDatacenterId(long maxDatacenterId) {
        long id = 0L;
        try {
            InetAddress ip = InetAddress.getLocalHost();
            NetworkInterface network = NetworkInterface.getByInetAddress(ip);
            if (network == null) {
                id = 1L;
            } else {
                byte[] mac = network.getHardwareAddress();//如果這行ide顯示錯誤,請檢查編譯版本(1.6以上才行)
                id = ((0x000000FF & (long) mac[mac.length - 1])
                        | (0x0000FF00 & (((long) mac[mac.length - 2]) << 8))) >> 6;
                id = id % (maxDatacenterId + 1);
            }
        } catch (Exception e) {
            System.out.println(" getDatacenterId: " + e.getMessage());
        }
        return id;
    }


    public static void main(String[] args) {

        IdWorker idWorker=new IdWorker(0,0);

        for(int i=0;i<100;i++){
            long nextId = idWorker.nextId();
            System.out.println(nextId);
        }


    }
}

數據中心編號以及機器編號:這只是一個邏輯上的劃分,並非要真正在機器上打上標籤。比如可以把A模塊定義爲數據中心1,其下集羣中的每個服務器實例就爲機器編號,你可以按照自然數區分,編號自己定,別重複就行。真正運用的時候,這個編號應該是事先都已經規劃好的,並將這兩個編號寫入項目的配置文件中,在項目中調用snowFlake算法的時候,讀取當前項目的數據中心編號以及機器編號

可以看到以上main方法中調用的時候傳入了兩個參數正式數據中心編號以及機器編號,共計10個bit位,所以可以保證在正確區分服務器編號及所屬數據中心的情況下,最多可以滿足2的十次方即1024臺規模的集羣環境,永不重複的id。

優點:

1)不依賴於數據庫,靈活方便,且性能優於數據庫。

2)ID按照時間在單機上是遞增的。

缺點:

1)在單機上是遞增的,但是由於涉及到分佈式環境,每臺機器上的時鐘不可能完全同步,也許有時候也會出現不是全局遞增的情況,而且集羣環境超過1024臺,這個算法理論上將不能保證100%不重複。

 

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

本文轉自:nick hao的博客園文章,針對snowFlake算法部分有改動。

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