分佈式惟一ID解決方案-雪花算法

閱讀大概須要3分鐘java

附源碼web

[toc]算法

封面圖

前言

單體架構的服務的日子已經一去不復返了。sql

當前系統業務和數據存儲的複雜度都在提高,分佈式系統是目前使用很是廣泛的解決方案。docker

全局惟一 ID 幾乎是全部設計系統時都會遇到的,全局惟一 ID 在存儲和檢索中有相當重要的做用。數據庫

ID生成器

在應用程序中,常常須要全局惟一的ID做爲數據庫主鍵。如何生成全局惟一ID?apache

首先,須要肯定全局惟一ID是整型仍是字符串?若是是字符串,那麼現有的UUID就徹底知足需求,不須要額外的工做。缺點是字符串做爲ID佔用空間大,索引效率比整型低。安全

若是採用整型做爲ID,那麼首先排除掉32位int類型,由於範圍過小,必須使用64位long型。服務器

採用整型做爲ID時,如何生成自增、全局惟一且不重複的ID?網絡

數據庫自增

數據庫自增 ID 是咱們在數據量較小的系統中常用的,利用數據庫的自增ID,從1開始,基本能夠作到連續遞增。Oracle能夠用 SEQUENCE,MySQL能夠用主鍵的 AUTO_INCREMENT,雖然不能保證全局惟一,但每一個表惟一,也基本知足需求。

數據庫自增ID的缺點是數據在插入前,沒法得到ID。數據在插入後,獲取的ID雖然是惟一的,但必定要等到事務提交後,ID纔算是有效的。有些雙向引用的數據,不得不插入後再作一次更新,比較麻煩。

在咱們開發過程當中,遇到一種 主主數據庫同步(簡單能夠理解爲,一樣的sql再另外一臺數據庫再執行一次)的場景,若是使用數據庫自增 ID,就會出現主鍵不一致、或主鍵衝突問題。

分佈式ID生成器

方案一:UUID

分佈式環境不推薦使用

uuid 是咱們比較先想到的方法,在 java.util;包中就有對應方法。這是一個具備rfc標準的uuid:https://www.ietf.org/rfc/rfc4122.txt

uuid 有很好的性能(本地調用),沒有網絡消耗。

可是,uuid 不易存儲(生成了字符串、存儲過長、不少場景不適用);信息不安全(基於 MAC 地址生成、可能會形成泄露,這個漏洞曾被用於尋找梅麗莎病毒的製做者位置。
);沒法保證遞增(或趨勢遞增);其餘博主反饋,截取前20位作惟一 ID ,在大數量(大概只有220w)狀況下會有重複問題。

UUID.randomUUID().toString()

方案二:snowflake(雪花算法)

這是目前使用較多分佈式ID解決方案,推薦使用

背景 Twitter 云云就不介紹了,就是前段時間封了懂王帳號的 Twitter。

算法介紹

SnowFlake算法生成id的結果是一個64bit大小的整數,它的結構以下圖:

snowflake-64bit

  • 1位,不用。二進制中最高位爲1的都是負數,可是咱們生成的id通常都使用整數,因此這個最高位固定是0

  • 41位,用來記錄時間戳(毫秒)。

    • 41位能夠表示 2^{41}-1 個數字,
    • 若是隻用來表示正整數(計算機中正數包含0),能夠表示的數值範圍是:0 至 2^{41}-1,減1是由於可表示的數值範圍是從0開始算的,而不是1。
    • 也就是說41位能夠表示 2^{41}-1 個毫秒的值,轉化成單位年則是 (2^{41}-1) / (1000 60 60 24 365) = 69 年
  • 10位,用來記錄工做機器id。

    • 能夠部署在 2^{10} = 1024 個節點,包括 5位 datacenterId 和 5位 workerId
    • 5位(bit)能夠表示的最大正整數是 2^{5}-1 = 31 ,便可以用 0、一、二、三、....31 這 32 個數字,來表示不一樣的 datecenterId 或 workerId
  • 12位,序列號,用來記錄同毫秒內產生的不一樣id。
    • 12位(bit)能夠表示的最大正整數是 2^{12}-1 = 4095 ,便可以用 0、一、二、三、....4094 這 4095 個數字,來表示同一機器同一時間截(毫秒)內產生的 4095 個 ID 序號。

因爲在 Java 中 64bit 的整數是 long 類型,因此在 Java 中 SnowFlake 算法生成的 id 就是 long 來存儲的。

SnowFlake能夠保證

  1. 同一臺服務器全部生成的id按時間趨勢遞增
  2. 整個分佈式系統內不會產生重複id(由於有datacenterId和workerId來作區分)

存在的問題:

  1. 機器ID(5位)和數據中心ID(5位)配置沒有解決,分佈式部署的時候會使用相同的配置,任然有ID重複的風險。
  2. 使用的時候須要實例化對象,沒有造成開箱即用的工具類。
  3. 強依賴機器時鐘,若是機器上時鐘回撥,會致使發號重複或者服務會處於不可用狀態。(這點在正常狀況下是不會發生的)

針對上面問題,這裏提供一種解決思路,workId 使用服務器 hostName 生成,dataCenterId 使用 IP 生成,這樣能夠最大限度防止 10 位機器碼重複,可是因爲兩個 ID 都不能超過 32,只能取餘數,仍是不免產生重複,可是實際使用中,hostName 和 IP 的配置通常連續或相近,只要不是恰好相隔 32 位,就不會有問題,何況,hostName 和 IP 同時相隔 32 的狀況更加是幾乎不可能的事,平時作的分佈式部署,通常也不會超過 10 臺容器。

生產上使用docker配置通常是一次編譯,而後分佈式部署到不一樣容器,不會有不一樣的配置。這種狀況就對上面提到的出現了不肯定狀況,這個在評論中會再出一篇參考文章。

源碼

Java 版雪花ID生成算法

package com.my.blog.website.utils;

import org.apache.commons.lang3.RandomUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemUtils;

import java.net.Inet4Address;
import java.net.UnknownHostException;

/**
 * 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 = 1489111610226L;

    /** 機器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;

    private static SnowflakeIdWorker idWorker;

    static {
        idWorker = new SnowflakeIdWorker(getWorkId(),getDataCenterId());
    }

    //==============================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("workerId can't be greater than %d or less than 0", maxWorkerId));
        }
        if (dataCenterId > maxDataCenterId || dataCenterId < 0) {
            throw new IllegalArgumentException(String.format("dataCenterId 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();
    }

    private static Long getWorkId(){
        try {
            String hostAddress = Inet4Address.getLocalHost().getHostAddress();
            int[] ints = StringUtils.toCodePoints(hostAddress);
            int sums = 0;
            for(int b : ints){
                sums += b;
            }
            return (long)(sums % 32);
        } catch (UnknownHostException e) {
            // 若是獲取失敗,則使用隨機數備用
            return RandomUtils.nextLong(0,31);
        }
    }

    private static Long getDataCenterId(){
        int[] ints = StringUtils.toCodePoints(SystemUtils.getHostName());
        int sums = 0;
        for (int i: ints) {
            sums += i;
        }
        return (long)(sums % 32);
    }

    /**
     * 靜態工具類
     *
     * @return
     */
    public static synchronized Long generateId(){
        long id = idWorker.nextId();
        return id;
    }

    //==============================Test=============================================
    /** 測試 */
    public static void main(String[] args) {
        System.out.println(System.currentTimeMillis());
        long startTime = System.nanoTime();
        for (int i = 0; i < 50000; i++) {
            long id = SnowflakeIdWorker.generateId();
            System.out.println(id);
        }
        System.out.println((System.nanoTime()-startTime)/1000000+"ms");
    }
}

參考原文:https://blog.csdn.net/xiaopeng9275/article/details/72123709

分享和在看是對我最大的鼓勵。我是pub哥,咱們下期見!

相關文章
相關標籤/搜索