1. 程式人生 > >分散式系統應用中生成全域性唯一ID的演算法(snowflake)----java 實現,單例模式

分散式系統應用中生成全域性唯一ID的演算法(snowflake)----java 實現,單例模式

概述

在分散式系統中,有很多的地方需要生成全域性id的場景,比方說,訂單模組,使用者id等。這種情況下大多數的做法是通過UUID來做處理。首先,UUID是36位的一個字串,相對來說是比較長的,一般我們採用的資料庫會是MySQL,因為大多數的情況下,我們都希望我們的資料是可以回滾的,那麼我們的資料表會採用innoDB,innoDB採用B+Tree實現其索引結構。所以一般對主鍵有以下的要求!

  • 越短越好——越短在一個Page中儲存的節點越多,檢索速度就越快。
  • 順序增長——如果每一條插入的資料的主鍵都比前面的主鍵大,那麼B-Tree上的節點也是順序增長的,不會造成頻繁的B-Tree分割。

越短越好是為了查詢的速度快,順序增長是為了插入速度快,所以UUID就感覺不是太優秀。

並且在後期資料量非常大的時候,我們採用分庫分表的情況下,可以更好的橫向擴充套件!

結構

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。

原始碼網上多的是,但是經過我的小範圍測試,如果例項太多的話,單個系統就會出現重複的情況。如果採用spring來管理的話,應該來說是沒有問題的,但是為了嚴謹期間,我把原始碼改造成一個單例模式的類。這樣,不管怎麼呼叫,單個系統生成的id永遠不會重複!

package main.java;

/**
 * 生成分散式系統唯一的主鍵id
 * string型別
 * Snowflake 的結構如下
 * 0-00000000 00000000 00000000 00000000 00000000 00000000 - 00000 - 0000 -000000000000
 * 第一位是符號為,41位表示當前時間戳跟定義好的時間戳的差值,5位
 * 41位的表示(1L << 41) / (1000L * 3600 * 24 * 365L) == 69年;
 * 指定10位的機器位,可以部署1024臺機器
 * 所有的資料加起來就是64位,正好是一個Long型資料
 */
public class SnowflakeIdWorker {
    /**
     * 開始時間戳
     */
    private final long startTime = 1420041600000L;
    /**
     * 機器id所佔的位數
     */
    private final long workIdBits = 5L;
    /**
     * 資料id所佔的位數
     */
    private final long dataIdBits = 5L;
    /**
     * 二進位制中,負數採用其絕對值的值得補碼得到
     * long型的-1 的值就32個1
     */
    private final long maxWorkerId = -1L ^ (-1L << workIdBits);
    /**
     * 同上
     */
    private final long maxDataGenID = -1L ^ (-1L << dataIdBits);
    /**
     * 12位的序列,表示1個毫秒內可生成 2的12次冪個數據,即4096個數據
     */
    private final long sequenceBits = 12L;
    /**
     * 資料id存放的位置應該是向左移動12位序列值和機器碼
     */
    private final long dataShiftBits = sequenceBits + workIdBits;
    /**
     * 時間戳存放的位置應該是從22位開始的,左移22位
     */
    private final long timestampLeftShift = dataShiftBits + dataIdBits;
    /**
     * 4095 生成序列的最大值
     */
    private final long maxSequence = -1 ^ (-1 << sequenceBits);
    /**
     * 機器碼id 小於31
     */
    private long worderId;
    /**
     * 資料中心id 小於31
     */
    private long dataId;
    /**
     * 毫秒內計數(0-4095)
     */
    private long sequence = 0L;
    /**
     * 上一次生成id的時間戳
     */
    private long lastTimeStamp = -1L;

    /**
     * 單例模式構造方法
     */
    private SnowflakeIdWorker() {
    }

    /**
     * 使用靜態內部類實現單例
     */
    private static class SnowflakeIdWorkerInstance {
        private static SnowflakeIdWorker INSTANCE = new SnowflakeIdWorker();
    }

    public static SnowflakeIdWorker getInstance() {
        return SnowflakeIdWorkerInstance.INSTANCE;
    }

    /**
     * 初始化並配置機器碼id和資料id
     *
     * @param workerId 0-31
     * @param dataId   0-31
     */
    public void init(long workerId, long dataId) {
        if (workerId > maxWorkerId || worderId < 0) {
            throw new IllegalArgumentException(String.format("worker Id can't greater than %d or less than 0", maxWorkerId));
        }
        if (dataId > maxDataGenID || dataId < 0) {
            throw new IllegalArgumentException(String.format("data Id can't greater than %d or less than 0", maxDataGenID));
        }
        this.worderId = workerId;
        this.dataId = dataId;
    }

    /**
     * 生成主鍵id,理論上應該在呼叫了init方法之後,呼叫生成的方式是有效的
     * 不然所有的id都預設是按照機器碼和資料id都是0的情況處理
     *
     * @return 8個位元組的長整型
     */
    public synchronized long genNextId() {
        long timeStamp = genTimeStamp();
        /**表示系統的時間修改了*/
        if (timeStamp < this.lastTimeStamp) {
            throw new RuntimeException(String.format("System clock moved;currentTimeStamp %d,lastTimeStamp = %d", timeStamp, this.lastTimeStamp));
        }
        if (timeStamp == this.lastTimeStamp) {
            /**檢視序列是否溢位*/
            this.sequence = (this.sequence + 1) & maxSequence;
            if (this.sequence == 0) {
                /**當出現溢位的時候,阻塞到下一個毫秒*/
                timeStamp = this.toNextMillis(this.lastTimeStamp);
            }
        } else {  /**此時表示時間戳跟最後的時間戳不一致,需要重置序列*/
            this.sequence = 0L;
        }
        this.lastTimeStamp = timeStamp;
        //通過移位或運算拼接組成64ID號
        return ((timeStamp - startTime) << timestampLeftShift)
                | (dataId << dataShiftBits)
                | (worderId << sequenceBits)
                | sequence;
    }

    /**
     * 生成當前時間戳,單獨寫一個方法的原因是,若之後的時候修改擴充套件,不影響之前的業務,
     * 只在這個方法裡面處理我們需要的資料
     *
     * @return
     */
    private long genTimeStamp() {
        return System.currentTimeMillis();
    }

    /**
     * 阻塞到下一個毫秒,直到獲得新的時間戳
     *
     * @param lastTimeStamp 上傳生成id的時間戳
     * @return
     */
    private long toNextMillis(long lastTimeStamp) {
        long timeStamp = this.genTimeStamp();
        while (timeStamp <= lastTimeStamp) {
            timeStamp = this.genTimeStamp();
        }
        return timeStamp;
    }

    //--------------------------test--------------------------------------
    public static void main(String[] args) {
        SnowflakeIdWorker worker = SnowflakeIdWorker.getInstance();
        /**第一次使用的時候希望初始化*/
        worker.init(30, 14);
        for (int i = 0; i < 10000; i++) {
            long workerId = worker.genNextId();
            System.out.println(workerId);
            System.out.println(Long.toBinaryString(workerId));
        }
    }

}

下面是測試的結果,紅框表示阻塞的情況!

測試結果

參考連結

結語:夜已深,休息啦,以後再有新的點子了跟大家分享!