若是再有人問你分佈式 ID,這篇文章丟給他

1.背景

在咱們的業務需求中一般有須要一些惟一的ID,來記錄咱們某個數據的標識:html

  • 某個用戶的ID
  • 某個訂單的單號
  • 某個信息的ID

一般咱們會調研各類各樣的生成策略,根據不一樣的業務,採起最合適的策略,下面我會討論一下各類策略/算法,以及他們的一些優劣點。java

2.UUID

UUID是通用惟一識別碼(Universally Unique Identifier)的縮寫,開放軟件基金會(OSF)規範定義了包括網卡MAC地址、時間戳、名字空間(Namespace)、隨機或僞隨機數、時序等元素。利用這些元素來生成UUID。node

UUID是由128位二進制組成,通常轉換成十六進制,而後用String表示。在java中有個UUID類,在他的註釋中咱們看見這裏有4種不一樣的UUID的生成策略:git

  • randomly: 基於隨機數生成UUID,因爲Java中的隨機數是僞隨機數,其重複的機率是能夠被計算出來的。這個通常咱們用下面的代碼獲取基於隨機數的UUID:
  • time-based:基於時間的UUID,這個通常是經過當前時間,隨機數,和本地Mac地址來計算出來,自帶的JDK包並無這個算法的咱們在一些UUIDUtil中,好比咱們的log4j.core.util,會從新定義UUID的高位和低位。
  • DCE security:DCE安全的UUID。
  • name-based:基於名字的UUID,經過計算名字和名字空間的MD5來計算UUID。

UUID的優勢:github

  • 經過本地生成,沒有通過網絡I/O,性能較快
  • 無序,沒法預測他的生成順序。(固然這個也是他的缺點之一)

UUID的缺點:面試

  • 128位二進制通常轉換成36位的16進制,太長了只能用String存儲,空間佔用較多。
  • 不能生成遞增有序的數字

適用場景:UUID的適用場景能夠爲不須要擔憂過多的空間佔用,以及不須要生成有遞增趨勢的數字。在Log4j裏面他在UuidPatternConverter中加入了UUID來標識每一條日誌。redis

3.數據庫主鍵自增

你們對於惟一標識最容易想到的就是主鍵自增,這個也是咱們最經常使用的方法。例如咱們有個訂單服務,那麼把訂單id設置爲主鍵自增便可。算法

優勢:數據庫

  • 簡單方便,有序遞增,方便排序和分頁

缺點:緩存

  • 分庫分表會帶來問題,須要進行改造。
  • 併發性能不高,受限於數據庫的性能。
  • 簡單遞增容易被其餘人猜想利用,好比你有一個用戶服務用的遞增,那麼其餘人能夠根據分析註冊的用戶ID來獲得當天你的服務有多少人註冊,從而就能猜想出你這個服務當前的一個大概情況。
  • 數據庫宕機服務不可用。

適用場景: 根據上面能夠總結出來,當數據量很少,併發性能不高的時候這個很適合,好比一些to B的業務,商家註冊這些,商家註冊和用戶註冊不是一個數量級的,因此能夠數據庫主鍵遞增。若是對順序遞加強依賴,那麼也可使用數據庫主鍵自增。

4.Redis

熟悉Redis的同窗,應該知道在Redis中有兩個命令Incr,IncrBy,由於Redis是單線程的因此能保證原子性。

優勢:

  • 性能比數據庫好,能知足有序遞增。

缺點:

  • 因爲redis是內存的KV數據庫,即便有AOF和RDB,可是依然會存在數據丟失,有可能會形成ID重複。
  • 依賴於redis,redis要是不穩定,會影響ID生成。

適用:因爲其性能比數據庫好,可是有可能會出現ID重複和不穩定,這一塊若是能夠接受那麼就可使用。也適用於到了某個時間,好比天天都刷新ID,那麼這個ID就須要重置,經過(Incr Today),天天都會從0開始加。

5.Zookeeper

利用ZK的Znode數據版本以下面的代碼,每次都不獲取指望版本號也就是每次都會成功,那麼每次都會返回最新的版本號:

Zookeeper這個方案用得較少,嚴重依賴Zookeeper集羣,而且性能不是很高,因此不予推薦。

6.數據庫分段+服務緩存ID

這個方法在美團的Leaf中有介紹,詳情能夠參考美團技術團隊的發佈的技術文章:Leaf——美團點評分佈式ID生成系統,這個方案是將數據庫主鍵自增進行優化。

biz_tag表明每一個不一樣的業務,max_id表明每一個業務設置的大小,step表明每一個proxyServer緩存的步長。 以前咱們的每一個服務都訪問的是數據庫,如今不須要,每一個服務直接和咱們的ProxyServer作交互,減小了對數據庫的依賴。咱們的每一個ProxyServer回去數據庫中拿出步長的長度,好比server1拿到了1-1000,server2拿到來 1001-2000。若是用完會再次去數據庫中拿。

優勢:

  • 比主鍵遞增性能高,能保證趨勢遞增。
  • 若是DB宕機,proxServer因爲有緩存依然能夠堅持一段時間。

缺點:

  • 和主鍵遞增同樣,容易被人猜想。
  • DB宕機,雖然能支撐一段時間可是仍然會形成系統不可用。

適用場景:須要趨勢遞增,而且ID大小可控制的,可使用這套方案。

固然這個方案也能夠經過一些手段避免被人猜想,把ID變成是無序的,好比把咱們生成的數據是一個遞增的long型,把這個Long分紅幾個部分,好比能夠分紅幾組三位數,幾組四位數,而後在創建一個映射表,將咱們的數據變成無序。

7.雪花算法-Snowflake

Snowflake是Twitter提出來的一個算法,其目的是生成一個64bit的整數:

  • 1bit:通常是符號位,不作處理
  • 41bit:用來記錄時間戳,這裏能夠記錄69年,若是設置好起始時間好比今年是2018年,那麼能夠用到2089年,到時候怎麼辦?要是這個系統能用69年,我相信這個系統早都重構了好屢次了。
  • 10bit:10bit用來記錄機器ID,總共能夠記錄1024臺機器,通常用前5位表明數據中心,後面5位是某個數據中心的機器ID
  • 12bit:循環位,用來對同一個毫秒以內產生不一樣的ID,12位能夠最多記錄4095個,也就是在同一個機器同一毫秒最多記錄4095個,多餘的須要進行等待下毫秒。

上面只是一個將64bit劃分的標準,固然也不必定這麼作,能夠根據不一樣業務的具體場景來劃分,好比下面給出一個業務場景:

  • 服務目前QPS10萬,預計幾年以內會發展到百萬。
  • 當前機器三地部署,上海,北京,深圳都有。
  • 當前機器10臺左右,預計將來會增長至百臺。

這個時候咱們根據上面的場景能夠再次合理的劃分62bit,QPS幾年以內會發展到百萬,那麼每毫秒就是千級的請求,目前10臺機器那麼每臺機器承擔百級的請求,爲了保證擴展,後面的循環位能夠限制到1024,也就是2^10,那麼循環位10位就足夠了。

機器三地部署咱們能夠用3bit總共8來表示機房位置,當前的機器10臺,爲了保證擴展到百臺那麼能夠用7bit 128來表示,時間位依然是41bit,那麼還剩下64-10-3-7-41-1 = 2bit,還剩下2bit能夠用來進行擴展。

適用場景:當咱們須要無序不能被猜想的ID,而且須要必定高性能,且須要long型,那麼就可使用咱們雪花算法。好比常見的訂單ID,用雪花算法別人就沒法猜想你天天的訂單量是多少。

7.1一個簡單的Snowflake

public class IdWorker{

    private long workerId;
    private long datacenterId;
    private long sequence = 0;
    /**
     * 2018/9/29日,今後時開始計算,能夠用到2089年
     */
    private long twepoch = 1538211907857L;

    private long workerIdBits = 5L;
    private long datacenterIdBits = 5L;
    private long sequenceBits = 12L;

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

    private long lastTimestamp = -1L;


    public IdWorker(long workerId, long datacenterId){
        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }
    public synchronized long nextId() {
        long timestamp = timeGen();
        //時間回撥,拋出異常
        if (timestamp < lastTimestamp) {
            System.err.printf("clock is moving backwards.  Rejecting requests until %d.", 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 = 0;
        }

        lastTimestamp = timestamp;
        return ((timestamp - twepoch) << timestampLeftShift) |
                (datacenterId << datacenterIdShift) |
                (workerId << workerIdShift) |
                sequence;
    }

    /**
     * 當前ms已經滿了
     * @param lastTimestamp
     * @return
     */
    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) {
        IdWorker worker = new IdWorker(1,1);
        for (int i = 0; i < 30; i++) {
            System.out.println(worker.nextId());
        }
    }

}

上面定義了雪花算法的實現,在nextId中是咱們生成雪花算法的關鍵。

7.2防止時鐘回撥

由於機器的緣由會發生時間回撥,咱們的雪花算法是強依賴咱們的時間的,若是時間發生回撥,有可能會生成重複的ID,在咱們上面的nextId中咱們用當前時間和上一次的時間進行判斷,若是當前時間小於上一次的時間那麼確定是發生了回撥,普通的算法會直接拋出異常,這裏咱們能夠對其進行優化,通常分爲兩個狀況:

  • 若是時間回撥時間較短,好比配置5ms之內,那麼能夠直接等待必定的時間,讓機器的時間追上來。
  • 若是時間的回撥時間較長,咱們不能接受這麼長的阻塞等待,那麼又有兩個策略:
  1. 直接拒絕,拋出異常,打日誌,通知RD時鐘回滾。
  2. 利用擴展位,上面咱們討論過不一樣業務場景位數可能用不到那麼多,那麼咱們能夠把擴展位數利用起來了,好比當這個時間回撥比較長的時候,咱們能夠不須要等待,直接在擴展位加1。2位的擴展位容許咱們有3次大的時鐘回撥,通常來講就夠了,若是其超過三次咱們仍是選擇拋出異常,打日誌。

經過上面的幾種策略能夠比較的防禦咱們的時鐘回撥,防止出現回撥以後大量的異常出現。下面是修改以後的代碼,這裏修改了時鐘回撥的邏輯:

最後

本文分析了各類生產分佈式ID的算法的原理,以及他們的適用場景,相信你已經能爲本身的項目選擇好一個合適的分佈式ID生成策略了。沒有一個策略是完美的,只有適合本身的纔是最好的。

這篇文章被我收錄於JGrowing,一個全面,優秀,由社區一塊兒共建的Java學習路線,若是您想參與開源項目的維護,能夠一塊兒共建,github地址爲:https://github.com/javagrowing/JGrowing 麻煩給個小星星喲。

最後打個廣告,若是你以爲這篇文章對你有文章,能夠關注個人技術公衆號,也能夠加入個人技術交流羣進行更多的技術交流。最近做者收集了不少最新的學習資料視頻以及面試資料,關注以後便可領取,你的關注和轉發是對我最大的支持,O(∩_∩)O。

相關文章
相關標籤/搜索