分佈式惟一id:snowflake算法思考

匠心零度 轉載請註明原創出處,謝謝!html

緣起

爲何會忽然談到分佈式惟一id呢?緣由是最近在準備使用RocketMQ,看看官網介紹: java

一句話,消息可能會重複,因此消費端須要作冪等。爲何消息會重複後續RocketMQ章節進行詳細介紹,本節重點不在這裏。git

爲了達到業務的冪等,必需要有這樣一個id存在,須要知足下面幾個條件:github

  • 同一業務場景要全局惟一。
  • 該id必須是在消息的發送方進行產生髮送到MQ。
  • 消費端根據該id進行判斷是否重複,確保冪等。

在那裏產生,和消費端進行判斷等和這個id沒有關係,這個id的要求就是局部惟一或者全局惟一便可,因爲這個id是惟一的,能夠用來當數據庫的主鍵,既然要作主鍵那麼以前剛恰好發過一篇文章:從開發者角度談Mysql(1):主鍵問題,文章重點提到爲何須要自增、或者趨勢自增的好處。(和Mysql數據存儲作法有關)。redis

那麼該id須要有2個特性:算法

  • 局部、全局惟一。
  • 趨勢遞增。

若是有方法能夠生成全局惟一(那麼在局部也必定惟一了),生成分佈式惟一id的方法有不少。你們能夠看看分佈式系統惟一ID生成方案彙總:http://www.cnblogs.com/haoxinyue/p/5208136.html(因爲微信不是他的地址都顯示不出來,因此把地址貼出來下),這個文章裏面提到了不少以及各各的優缺點。 本文關注重點是snowflake算法,該算法實現獲得的id就知足上面提到的2點。sql

snowflake算法

snowflake是Twitter開源的分佈式ID生成算法,結果是一個long型的ID。其核心思想是:使用41bit做爲毫秒數,10bit做爲機器的ID(5個bit是數據中心,5個bit的機器ID),12bit做爲毫秒內的流水號(意味着每一個節點在每毫秒能夠產生 4096 個 ID),最後還有一個符號位,永遠是0。數據庫

該算法實現基本就是二進制操做,若是二進制不熟悉的能夠看看我以前寫的相關文章:java二進制相關基礎二進制實戰技巧安全

這個算法單機每秒內理論上最多能夠生成1000*(2^12),也就是409.6萬個ID,(吼吼,這個得了的快啊)。服務器

java實現代碼基本上就是相似這樣的(都差很少,基本就是二進制位操做): 參考:https://www.cnblogs.com/relucent/p/4955340.html

/** * 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);
        }
    }
}
複製代碼

優勢:

  • 快(哈哈,天下武功惟快不破)。
  • 沒有啥依賴,實現也特別簡單。
  • 知道原理以後能夠根據實際狀況調整各各位段,方便靈活。

缺點:

  • 只能趨勢遞增。(有些也不叫缺點,網上有些若是絕對遞增,競爭對手中午下單,次日在下單便可大概判斷該公司的訂單量,危險!!!)
  • 依賴機器時間,若是發生回撥會致使可能生成id重複。 下面重點討論時間回撥問題。

snowflake算法時間回撥問題思考

因爲存在時間回撥問題,可是他又是那麼快和簡單,咱們思考下是否能夠解決呢? 零度在網上找了一圈沒有發現具體的解決方案,可是找到了一篇美團不錯的文章:Leaf——美團點評分佈式ID生成系統(https://tech.meituan.com/MT_Leaf.html)文章很不錯,惋惜並無提到時間回撥如何具體解決。下面看看零度的一些思考:

分析時間回撥產生緣由

第一:人物操做,在真實環境通常不會有那個傻逼幹這種事情,因此基本能夠排除。 第二:因爲有些業務等須要,機器須要同步時間服務器(在這個過程當中可能會存在時間回撥,查了下咱們服務器通常在10ms之內(2小時同步一次))。

解決方法

  1. 因爲是分佈在各各機器本身上面,若是要幾臺集中的機器(而且不作時間同步),那麼就基本上就不存在回撥可能性了(曲線救國也是救國,哈哈),可是也的確帶來了新問題,各各結點須要訪問集中機器,要保證性能,百度的uid-generator產生就是基於這種狀況作的(每次取一批迴來,很好的思想,性能也很是不錯)https://github.com/baidu/uid-generator。 若是到這裏你採納了,基本就沒有啥問題了,你就不須要看了,若是你還想看看零度本身的思考能夠繼續往下看看(零度的思考只是一種思考 可能也不必定好,期待你的交流。),uid-generator我尚未細看,可是看測試報告很是不錯,後面有空的確要好好看看。

  2. 下面談談零度本身的思考,以前也大概和美團Leaf做者交流了下,的確零度的這個能夠解決一部分問題,可是引入了一些其餘問題和依賴。是零度的思考,期待更多的大佬給點建議。

時間問題回撥的解決方法:

  1. 當回撥時間小於15ms,就等時間追上來以後繼續生成。
  2. 當時間大於15ms時間咱們經過更換workid來產生以前都沒有產生過的來解決回撥問題。

首先把workid的位數進行了調整(15位能夠達到3萬多了,通常夠用了)

Snowflake算法稍微調整下位段:

  • sign(1bit) 固定1bit符號標識,即生成的暢途分佈式惟一id爲正數。
  • delta seconds (38 bits) 當前時間,相對於時間基點"2017-12-21"的增量值,單位:毫秒,最多可支持約8.716年
  • worker id (15 bits) 機器id,最多可支持約3.28萬個節點。
  • sequence (10 bits) 每秒下的併發序列,10 bits,這個算法單機每秒內理論上最多能夠生成1000*(2^10),也就是100W的ID,徹底能知足業務的需求。

因爲服務無狀態化關係,因此通常workid也並不配置在具體配置文件裏面,看看我這篇的思考,爲何須要無狀態化。高可用的一些思考和理解,這裏咱們選擇redis來進行中央存儲(zk、db)都是同樣的,只要是集中式的就能夠。

下面到了關鍵了: 如今我把3萬多個workid放到一個隊列中(基於redis),因爲須要一個集中的地方來管理workId,每當節點啓動時候,(先在本地某個地方看看是否有 借鑑弱依賴zk 本地先保存),若是有那麼值就做爲workid,若是不存在,就在隊列中取一個當workid來使用(隊列取走了就沒了 ),當發現時間回撥太多的時候,咱們就再去隊列取一個來當新的workid使用,把剛剛那個使用回撥的狀況的workid存到隊列裏面(隊列咱們每次都是從頭取,從尾部進行插入,這樣避免剛剛a機器使用又被b機器獲取的可能性)。

有幾個問題值得思考:

  • 若是引入了redis爲啥不用redis下發id?(查看分佈式系統惟一ID生成方案彙總會得到答案,咱們這裏僅僅是用來一致性隊列的,能作一致性隊列的基本均可以)。

  • 引入redis就意味着引入其餘第三方的架構,作基礎框架最好是不要引用(越簡單越好,目前還在學習提升)。

  • redis一致性怎麼保證?(redis掛了怎麼辦,怎麼同步,的確值得商榷。可能會引入會引入不少新的小問題)。

總結

因此選擇相似百度的那種作法比較好,集中以後批取,零度的思考雖然思考了,可是從基礎組件來看並非特別合適,可是也算一種思路吧。期待與大佬們的交流。


若是讀完以爲有收穫的話,歡迎點贊、關注、加公衆號【匠心零度】,查閱更多精彩歷史!!!

相關文章
相關標籤/搜索