SnowFlake算法

分佈式id生成算法(SnowFlake算法)

分佈式id生成算法的有很多種,Twitter的SnowFlake就是其中經典的一種。

概述

SnowFlake算法生成id的結果是一個64bit大小的整數,它的結構如下圖:
在這裏插入圖片描述

  • 1位,不用。二進制中最高位爲1的都是負數,但是我們生成的id一般都使用整數,所以這個最高位固定是0。
  • 41位,用來記錄時間戳(毫秒)。
    • 41位可以表示241−1個數字,如果只用來表示正整數(計算機中正數包含0),可以表示的數值範圍是:0至241−1,減1是因爲可表示的數值範圍是從0開始算的,而不是1。也就是說41位可以表示241−1個毫秒的值,轉化成單位年則是(2^41−1)/(1000∗60∗60∗24∗365)=69年。
  • 10位,用來記錄工作機器id。(此處可根據需要調整datacenterId和workerIdde位數,位數和爲10即可)。
    • 可以部署在210=1024個節點,包括5位datacenterId(機房id)5位workerId(機器id)。
    • 5位(bit)可以表示的最大正整數是25−1=31,即可以用0、1、2、3、…31這32個數字,來表示不同的datecenterId或workerId。
  • 12位,序列號,用來記錄同毫秒內產生的不同id。
    • 12位(bit)可以表示的最大正整數是212−1=4095,即可以用0、1、2、3、…4094這4095個數字,來表示同一機器同一時間截(毫秒)內產生的4095個ID序號。

由於在Java中64bit的整數是long類型,所以在Java中SnowFlake算法生成的id就是long來存儲的。

SnowFlake可以保證:

  • 所有生成的id按時間趨勢遞增。
  • 整個分佈式系統內不會產生重複id(因爲有datacenterId和workerId來做區分)。

Java代碼實現

package com.fyj.util;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @author fanyongju
 * @Title: IdWorker
 * @Description: TODO
 * @date 2018/10/1219:35
 */
public class IdWorker {
    private long workerId;
    private long datacenterId;
    private long sequence;

    public IdWorker(long workerId, long datacenterId, long sequence) {
        // sanity check for workerId
        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;
        this.sequence = sequence;
    }

    private long twepoch = 1539920215000L;//初始毫秒時間戳2018-10-19 11:36:55

    private long workerIdBits = 7L;//機器選擇2^7=128(每個機房最多128臺機器)機器id爲0~127
    private long datacenterIdBits = 3L;//機房選擇2^3=8(最多8個機房)機房id爲0~7
    private long maxWorkerId = -1L ^ (-1L << workerIdBits);//^異或:兩個比較的位不同時其結果是1,相同結果爲0
    private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
    private long sequenceBits = 12L;

    private long workerIdShift = sequenceBits;
    private long datacenterIdShift = sequenceBits + workerIdBits;
    private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
    private long sequenceMask = -1L ^ (-1L << sequenceBits);

    private long lastTimestamp = -1L;

    public long getWorkerId() {
        return workerId;
    }

    public long getDatacenterId() {
        return datacenterId;
    }

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

    //用同步鎖保證線程安全
    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;
            //sequence等於0說明毫秒內序列已經增長到最大值
            if (sequence == 0) {
                //阻塞到下一個毫秒,獲得新的時間戳
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {//時間戳改變,毫秒內序列重置
            sequence = 0;//這種重置可能會導致id分佈不均,後期如果需要的話,建議改爲0-4095之間的隨機數
        }
        //記錄上次生成ID的時間截
        lastTimestamp = timestamp;
        /*
        移位並通過或運算拼到一起組成64位的ID,將timestamp減去指定的初始時間戳twepoch結果左移timestampLeftShift(22)位
        ,然後與上左移datacenterIdShift(19)的datacenterId,再與上左移workerIdShift(12)位的workerId,再與上sequence。
        注意:此處轉換成二進制進行操作
        */
        return ((timestamp - twepoch) << timestampLeftShift) |
                (datacenterId << datacenterIdShift) |
                (workerId << workerIdShift) |
                sequence;
    }

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

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

    public static void main(String[] args) {//通過生成的id反解出dataCenId和workId
        IdWorker idWorker = new IdWorker(39, 7, 0);
        while (true) {//請手動終止
            Long id = idWorker.nextId();
            System.out.println("generate id is:" + id);
            StringBuilder dw = new StringBuilder(Long.toBinaryString(id / 4096 % 1024));
            StringBuilder time = new StringBuilder(Long.toBinaryString(id / (4096 * 1024)));
            if (dw.length() < 10) {
                int l = 10 - dw.length();
                for (int i = 0; i < l; i++) {
                    dw.insert(0, "0");
                }
            }
            String dataCenterId = dw.substring(0, 3);
            String workId = dw.substring(3);
            Date date = new Date((Long.parseLong(time.toString(), 2) + 1288834974657L));
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
            System.out.println("time:" + sdf.format(date)
                    + ", dataCenterId:" + Long.parseLong(dataCenterId, 2)
                    + ", workId:" + Long.parseLong(workId, 2)
                    + ", sequence:" + Long.parseLong(Long.toBinaryString(id % 4096), 2));
        }
    }

}

代碼解釋

  • 獲得單一機器的下一個序列號,使用Synchronized控制併發,而非CAS的方式,是因爲CAS不適合併發量非常高的場景。
  • 如果當前毫秒在一臺機器的序列號已經增長到最大值4095,則使用while循環等待直到下一毫秒。
  • 如果當前時間小於記錄的上一個毫秒值,則說明這臺機器的時間回撥了,拋出異常。但如果這臺機器的系統時間在啓動之前回撥過,那麼有可能出現ID重複的危險。

SnowFlake算法的優點:

  • 生成ID時不依賴於DB,完全在內存生成,高性能高可用。
  • ID呈趨勢遞增,後續插入索引樹的時候性能較好。

SnowFlake算法的缺點:

  • 依賴於系統時鐘的一致性。如果某臺機器的系統時鐘回撥,有可能造成ID衝突,或者ID亂序。

動動小手點個贊吧,謝謝了

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