1. 程式人生 > >分散式ID生成

分散式ID生成

幾乎所有的業務系統,都會有很多表記錄,都有生成一個記錄標識的需求,或者直接使用資料自帶的自增鍵,或者自己開發(一般大公司有中介軟體部門提供元件或服務),作為工程師也是我們要掌握的技能,過往實踐中,碰到過不少ID生成場景,如:

  • 資料量不大(資料在千萬以下),寫入併發未達到資料庫上限,建單表,主鍵使用資料庫的auto_increment足矣;
  • 業務日誌,資料寫入海量(每天可能達到百億級,如果到家的日誌平臺-守望者,阿里的鷹眼等),寫入併發高,主鍵適合使用UUID;
  • 資料量大(如訂單 交易 積分等),使用了分庫分表,ID的生成要求:生成全域性唯一,高效能,容災,可擴充套件,這裡就需要分散式ID;
  • 其他:有脫敏性要求的,防競品收集商家數量 訂單數量 交易筆數等商業機密,要求ID雜湊,如商家ID,訂單ID,交易ID 不宜使用資料庫的自增鍵。

分散式ID,顧名思義在分散式環境下生成全域性唯一的ID。有twitter提出的Snowflake(雪花)演算法,Flicker演算法,有在Flicker思想上進一步的資料庫+本地快取演算法。

Snowflake

演算法

image.png

演算法理解起來還是比較簡單:

  1. 41 bits: Timestamp(毫秒級)
  2. 10 bits:機器ID(一般是單元機房ID 5 bits + 機器ID 5 bits),也說是邏輯分片的l
  3. 12 bits: 可單機生成12位bit的自增序列

JAVA實現

/**
 * Created by 登程 on 2018/1/24.
 */
public class Snowflake {
    /**
     * 機器ID佔用位數
     */
    private static final int WORK_ID_BIT_SIZE = 10;
    /**
     * 機器ID最大數
     */
    private static final int WORK_ID_MAX_VAL = -1 ^ (-1 << WORK_ID_BIT_SIZE);
    private int workId;
    /**
     * 序列佔用最大位數
     */
    private static final int SEQUENCE_BIT_SIZE = 12;
    /**
     * 序列最大值(4098)
     */
    private static final int SEQUENCE_MAX_VAL = -1 ^ (-1 << SEQUENCE_BIT_SIZE);
    private int sequence = 0;
    private long lastTimestamp = 0L;
    /**
     * 最小日期1970-01-01
     */
    private static final long MIN_TIME = 1288834974657L;

    public Snowflake(int workId) {
        if (workId > WORK_ID_MAX_VAL || workId < 0) {
            throw new IllegalArgumentException("workId引數錯誤");
        }
        this.workId = workId;
    }

    public synchronized long nextId() {
        long currentTime = getTimestamp();
        if (lastTimestamp > currentTime) {
            throw new IllegalArgumentException("time clock is error!");
        }
        if (currentTime == lastTimestamp) {
            this.sequence = (this.sequence + 1) & SEQUENCE_MAX_VAL;
            if (sequence > SEQUENCE_MAX_VAL) {
                throw new IllegalArgumentException("併發量大,sequence溢位!");
            }
        } else {
            this.sequence = 0;
            lastTimestamp = currentTime;
        }

        return ((lastTimestamp - MIN_TIME) << (WORK_ID_BIT_SIZE+SEQUENCE_BIT_SIZE)) | (this.workId << WORK_ID_BIT_SIZE) | sequence;
    }

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

測試程式碼如下

/**
 * Created by 登程 on 2018/1/25.
 */
public class TestSnowflake {
    private static ExecutorService threadPool = Executors.newFixedThreadPool(200);
    private static Snowflake snowflake2 = new Snowflake(11);
    private static Snowflake snowflake1 = new Snowflake(10);
    public static void main(String[] args) {

        for (int i = 0; i < 100000; i++) {
            threadPool.execute(new IdGenThread(snowflake2));
            threadPool.execute(new IdGenThread(snowflake1));
        }
        threadPool.shutdown();
        while (!threadPool.isTerminated()){
            try {
                Thread.sleep(100L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

/**
 * Created by 登程 on 2018/1/25.
 */
public class IdGenThread implements Runnable {
    private Snowflake snowflake;

    IdGenThread(Snowflake snowflake) {
        this.snowflake = snowflake;
    }

    @Override
    public void run() {
        System.out.println(snowflake.nextId());
    }
}

優點缺點

優點和缺點都明顯

優點

  • 生成Long型易操作,有序
  • 效能高效(單機支援4098個併發生成ID,對於大部分業務來說足夠了)

缺點

  • 維護成本:維護機器ID(大公司有統一機器部署服務,單元機房和機器很容易讀取到,小公司維護起來較為麻煩,可能需要在每臺機器上序號)
  • 沒有一個全域性時鐘,難以保證時序

Flicker演算法

不同分庫設定不同的起始值和步長。Flicker啟用了兩臺資料庫伺服器來生成ID,通過區分auto_increment的起始值和步長來生成奇偶數的ID。

資料庫配置

Sequence Server1:
auto-increment-increment = 2
auto-increment-offset = 1
 
Sequence Server2:
auto-increment-increment = 2
auto-increment-offset = 2

image.png

如何路由到不同的資料庫上,做到負載均衡:如果保證自增順序,可以考慮利用ZK或redis等做分散式鎖來保證;不考慮自增順序,可以考慮RPC類似的負載原理。

  • 優勢:利用mysql自增id
  • 缺點:運維成本比較高,資料擴容時需要重新設定步長;資料庫的寫壓力依然很大,每次生成ID都要訪問資料庫。

基於資料庫更新+記憶體分配

在資料庫中維護一個ID,獲取下一個ID時,會對資料庫進行ID=ID+{步長} WHERE ID=XX,如果更新成功,則表示可以拿到{步長} 個ID,在記憶體中進行分配,單機最多可生產{步長} 個ID,超時或者達到{步長} 個ID後,則繼續對資料庫進行獲取。
在分散式環境下,容災和擴容是需要考慮,概括起來就是:生成id的資料庫可以是多機,其中的一個或者多個數據庫掛了,不能影響id獲取,保證高可用.

利用多庫的id生成方案: 每個資料庫只拿自己的那一段id. 如下圖,sample_db_1-sample_db_4是用來生成全域性唯一id的4個數據庫(一般在業務資料庫中建立,可保證同生共死),那麼每個資料庫對於同一個id有一個起始值,比如步長是1000.

資料庫 取值範圍(步長:1000)
sample_db_1 0
sample_db_2 1000
sample_db_3 2000
sample_db_4 3000

應用某臺伺服器獲取ID的時候,隨機取到了sample_db_2,那麼這臺機器會拿到2000-2999這一千個id(初始值是2000,在單機記憶體中將其視為臨界資源保證自增),當然而這個時候4個數據庫上id起始值會變成下圖所示:

資料庫 取值範圍(步長:1000)
sample_db_1 0
sample_db_2 5000
sample_db_3 2000
sample_db_4 3000

   注意到,下次從sample_db_2上取得的id就變成了5000-5999. 這樣,完全避免了多機上取id的重複.比如sample_db_2只會取到1000-1999,5000-5999,9000-9999, 13000-13999…sample_db_1,sample_db_3,sample_db_4其他資料庫取值範圍也一樣,這樣就不會相互重疊,資料唯一性得到保證。
   如果擴容,如將資料庫擴充套件成5個(建議設定初始值,避免重試多次),sample_db_2的取值範圍就會是1000-1999,6000-6999,11000-11999...一個小段時間存在2臺伺服器有生成統一ID的可能,建議重啟機器並做重試(有重複異常時,取ID),這樣問題解決。

優勢:高可用
缺點:無法保證自增順序;需要根據合適的寫入QPS來判斷步長。



作者:alivs
連結:https://www.jianshu.com/p/ab8f3a6b84d1
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。