分散式系統中ID的需求

南方菇涼發表於2019-04-26

在過去單機系統中,生成唯一ID比較簡單,可以使用MySQL的自增主鍵或者Oracle中sequence。在現在的大型高併發分散式系統中,以上策略就會有問題了,因為不同的資料庫會部署到不同的機器上,一般都是多主例項,而且再加上高併發的話,就會有重複ID的情況了。 在分散式系統中,ID有如下需求: 1.全域性唯一性:不能出現重複的ID,最基本的要求。 2.資料遞增:保證我的下一ID一定大於上一個ID 3.資訊保安:防止惡意使用者規矩id的規則來獲取資料

ID生成的方案##

1.UUID

UUID是指在一臺機器在同一時間中生成的數字在所有機器中都是唯一的。演算法的核心思想是結合機器的網路卡、當地時間、一個隨即數來生成UUID。從理論上講,如果一臺機器每秒產生10000000個GUID,則可以保證(概率意義上)3240年不重複。 UUID由以下幾部分的組合: 1.當前日期和時間; 2.時鐘序列; 3.全域性唯一的IEEE機器識別號,如果有網路卡,從網路卡MAC地址獲得,沒有網路卡以其他方式獲; 標準的UUID格式為:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (8-4-4-4-12),以連字號分為五段形式的36個字元,示例:cbfc8eef-5c71-4f4d-bf4b-42f93128ba77 Java標準類庫中已經提供了UUID的API。

#Java中生成uuid UUID.randomUUID() 優點: 1.本地生成ID,不需要進行遠端呼叫,時延低 2.擴充套件性好,基本可以認為沒有效能上限 缺點: 1.無法保證趨勢遞增 2.uuid過長,往往用字串表示,作為主鍵建立索引查詢效率低,常見優化方案為“轉化為兩個uint64整數儲存”或者“折半儲存”(折半後不能保證唯一性) 3.資訊不安全,基於MAC地址生成UUID的演算法可能會造成MAC地址洩露,這個漏洞曾被用於尋找梅麗莎病毒的製作者位置。

2.Snowflake雪花演算法

twitter在把儲存系統從MySQL遷移到Cassandra的過程中由於Cassandra沒有順序ID生成機制,於是自己開發了一套全域性唯一ID生成服務:Snowflake。 Snowflake的結構如下(每部分用-分開):

分散式系統中ID的需求

雪花ID生成的是一個64位的二進位制正整數,然後轉換成10進位制的數。64位二進位制數由如下部分組成: 1位識別符號:始終是0,由於long基本型別在Java中是帶符號的,最高位是符號位,正數是0,負數是1,所以id一般是正數,最高位是0。 41位時間戳:41位時間截不是儲存當前時間的時間截,而是儲存時間截的差值(當前時間截 - 開始時間截 )得到的值,這裡的的開始時間截,一般是我們的id生成器開始使用的時間,由我們程式來指定的。 10位機器標識碼:可以部署在1024個節點,如果機器分機房(IDC)部署,這10位可以由 5位機房ID + 5位機器ID 組成。 12位序列:毫秒內的計數,12位的計數順序號支援每個節點每毫秒(同一機器,同一時間截)產生4096個ID序號 優點 : 1.簡單高效,生成速度快; 2.時間戳在高位,自增序列在低位,整個ID是趨勢遞增的,按照時間有序遞增。 3.靈活度高,可以根據業務需求,調整bit位的劃分,滿足不同的需求。 缺點: 1.依賴機器的時鐘,如果伺服器時鐘回撥,會導致重複ID生成。 2.在分散式環境上,每個伺服器的時鐘不可能完全同步,有時會出現不是全域性遞增的情況。

snowflake Java實現 `public class SnowflakeIdWorker {

/** 開始時間截 (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;

/**
 * 建構函式
 * @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;
}

/**
 * 獲得下一個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();
}


/** 測試 */
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);
    }
}
複製程式碼

}`

3.Mysql自增機制

主要思路是採用資料庫自增ID + replace_into實現唯一ID的獲取。 create table t_global_id(     id bigint(20) unsigned not null auto_increment,     stub char(1) not null default '',     primary key (id),     unique key stub (stub) ) engine=MyISAM;

#每次業務可以使用以下SQL讀寫MySQL得到ID號 replace into t_golbal_id(stub) values('a'); select last_insert_id(); replace into跟insert功能類似,不同點在於:replace into首先嚐試插入資料列表中,如果發現表中已經有此行資料(根據主鍵或唯一索引判斷)則先刪除,再插入。否則直接插入新資料。 當然為了避免資料庫的單點故障,最少需要兩個資料庫例項,通過區分auto_increment的起始值和步長來生成奇偶數的ID。如下: `Server1: auto-increment-increment = 2 auto-increment-offset = 1

Server2: auto-increment-increment = 2 auto-increment-offset = 2` 優點:簡單,充分藉助資料庫的自增ID機制,可靠性高,生成有序的ID。 缺點: I1.生成依賴資料庫單機的讀寫效能。 2依賴資料庫,當資料庫異常時整個系統不可用。

4.Redis原子計數器

Redis實現了一個原子操作INCR和INCRBY實現遞增的操作。當使用資料庫效能不夠時,可以採用Redis來代替,同時使用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 優點: 1.不依賴於資料庫,靈活方便,且效能優於資料庫。 2.數字ID天然排序,對分頁或者需要排序的結果很有幫助。 缺點: 1.如果系統中沒有Redis,還需要引入新的元件,增加系統複雜度。 2.需要編碼和配置的工作量比較大。這個都不是最大的問題。 關於分散式全域性唯一ID的生成,各個網際網路公司有很多實現方案,比如美團點評的Leaf-snowflake,用zookeeper解決了各個伺服器時鐘回撥的問題,弱依賴zookeeper。以及Leaf-segment類似上面資料庫批量ID獲取的方案。

相關文章