Twitter的雪花算法(snowflake)自增ID


前言
  這個問題源自於,我想找一個分布式下的ID生成器。
  這個最簡單的方案是,數據庫自增ID。為啥不用咧?有這么幾點原因,一是,會依賴於數據庫的具體實現,比如,mysql有自增,oracle沒有,得用序列,mongo似乎也沒有他自己有個什么ID,sqlserver貌似有自增等等,有些不穩定因素,因為ID生成是業務的核心基礎。當然,還有就是性能,自增ID是連續的,它就依賴於數據庫自身的鎖,所以數據庫就有瓶頸。當然了,多台數據庫加某種間隔也是可用的,但是,運維維護會很復雜,因為它不是內聚的解決方案。而且,很難提前獲得下一個ID。
  后來,我用過一段時間在數據庫表里進行記錄來進行自增。這個的優勢是,我可以提前獲得下一個ID,而且,某個進程里可以一次獲取一批,減少鎖的依賴,雖然進程間的不重復依然是基於數據庫事務隔離的,但是,依賴小了,瓶頸小了。這個方案其實挺好的,我依然也會繼續用,主要是,它可以生成數字字母混合的編劇號,而且基本可控。但是,我數據庫主鍵為了效率和空間成本,基本會選用long,基本順序生成就可以了,所以,使用這種帶持久化的方案,會顯得很重。起項目的時候,也是,需要先建立對應的表,然后再把代碼或者jar包引進去,然后再用,比較重。最好就是能夠直接生成,沒有那么多依賴。
  然后,我從我上司那里聽到了twitter的這個算法。其實,我上司有個實現,我這個就是基於他的改的,但是,他的有兩個值是配置的,我還是嫌麻煩,於是就動手把那兩個值變成了從機器與進程獲取,就有了這個版本。

思路
  說實話,我也就聽了這么個算法的名字,沒正經看過原算法,但是,我上司說他代碼是網上抄的,所以,這個算法名字我還是不敢丟,下面我們說說整體的思路。
  整個ID的構成大概分為這么幾個部分,時間戳差值,機器編碼,進程編碼,序列號。java的long是64位的從左向右依次介紹是:時間戳差值,在我們這里占了42位;機器編碼5位;進程編碼5位;序列號12位。所有的拼接用位運算拼接起來,於是就基本做到了每個進程中不會重復了。

代碼

package nature.framework.core.common;

import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Enumeration;

/**
 * 主鍵生成器
 *
 * @author nature
 * @create 2017-12-22 10:58
 */
public class KeyWorker {
    private final static long twepoch = 12888349746579L;
    // 機器標識位數
    private final static long workerIdBits = 5L;
    // 數據中心標識位數
    private final static long datacenterIdBits = 5L;

    // 毫秒內自增位數
    private final static long sequenceBits = 12L;
    // 機器ID偏左移12位
    private final static long workerIdShift = sequenceBits;
    // 數據中心ID左移17位
    private final static long datacenterIdShift = sequenceBits + workerIdBits;
    // 時間毫秒左移22位
    private final static long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
    //sequence掩碼,確保sequnce不會超出上限
    private final static long sequenceMask = -1L ^ (-1L << sequenceBits);
    //上次時間戳
    private static long lastTimestamp = -1L;
    //序列
    private long sequence = 0L;
    //服務器ID
    private long workerId = 1L;
    private static long workerMask= -1L ^ (-1L << workerIdBits);
    //進程編碼
    private long processId = 1L;
    private static long processMask=-1L ^ (-1L << datacenterIdBits);
    private static KeyWorker keyWorker = null;

    static{
        keyWorker=new KeyWorker();
    }
    public static synchronized long nextId(){
        return keyWorker.getNextId();
    }

    private KeyWorker() {

        //獲取機器編碼
        this.workerId=this.getMachineNum();
        //獲取進程編碼
        RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
        this.processId=Long.valueOf(runtimeMXBean.getName().split("@")[0]).longValue();

        //避免編碼超出最大值
        this.workerId=workerId & workerMask;
        this.processId=processId & processMask;
    }

    public synchronized long getNextId() {
        //獲取時間戳
        long timestamp = timeGen();
        //如果時間戳小於上次時間戳則報錯
        if (timestamp < lastTimestamp) {
            try {
                throw new Exception("Clock moved backwards.  Refusing to generate id for " + (lastTimestamp - timestamp) + " milliseconds");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        //如果時間戳與上次時間戳相同
        if (lastTimestamp == timestamp) {
            // 當前毫秒內,則+1,與sequenceMask確保sequence不會超出上限
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) {
                // 當前毫秒內計數滿了,則等待下一秒
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0;
        }
        lastTimestamp = timestamp;
        // ID偏移組合生成最終的ID,並返回ID
        long nextId = ((timestamp - twepoch) << timestampLeftShift) | (processId << datacenterIdShift) | (workerId << workerIdShift) | sequence;
        return nextId;
    }

    /**
     * 再次獲取時間戳直到獲取的時間戳與現有的不同
     * @param lastTimestamp
     * @return 下一個時間戳
     */
    private long tilNextMillis(final long lastTimestamp) {
        long timestamp = this.timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = this.timeGen();
        }
        return timestamp;
    }

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

    /**
     * 獲取機器編碼
     * @return
     */
    private long getMachineNum(){
        long machinePiece;
        StringBuilder sb = new StringBuilder();
        Enumeration<NetworkInterface> e = null;
        try {
            e = NetworkInterface.getNetworkInterfaces();
        } catch (SocketException e1) {
            e1.printStackTrace();
        }
        while (e.hasMoreElements()) {
            NetworkInterface ni = e.nextElement();
            sb.append(ni.toString());
        }
        machinePiece = sb.toString().hashCode();
        return machinePiece;
    }
}

  

代碼解讀
整體設計
  為了最大程度的減少配置,方便實用,這個模塊,我設計成了單例模式。之所以沒有直接使用static方法,還是希望可以控制整個模塊的生命周期,但是,模塊的初始化,我使用了static塊,因為它沒有任何依賴。
  有個static的nextId方法,可以直接獲得下一個ID,這個方法是線程安全的。同時這個模塊的使用就是這么簡單粗暴,也不用配置bean。

ID生成邏輯
  我們先看最后一步:long nextId = ((timestamp - twepoch) << timestampLeftShift) | (processId << datacenterIdShift) | (workerId << workerIdShift) | sequence;
  這句話什么意思呢?
  timestamp - twepoch:時間戳減去一個時間戳,獲得一個差值。
  ((timestamp - twepoch) << timestampLeftShift):timestampLeftShift是22,這個操作是將這個差值向左移22位,左移空出來的會自動補0,我們就有了22位的空間了。
  后面可以看到三個|符號,與操作會把1都加進來,而我們后面的數也都在各自的位上才有1,那么|操作就把這些數合進來了。
  (processId << datacenterIdShift):進程編碼左移datacenterIdShift,這個是17位,而processId最多是5位,於是剛好填滿空位
  (workerId << workerIdShift):與進程編碼類似,機器編碼也是5位,左移12位
  sequence最大12位。

如何確保不超出位數限制
  前面的邏輯中,我們說了很多不超出位數限制啥的內容,那么,具體是怎么做到的呢?我們拿workerId舉個例子:
  this.workerId=workerId & workerMask;
  這是我們確保workerId不超過5位的語句,什么意思呢?不經常操作位運算真看不懂。我們先看看workerMask是啥。
  private static long workerMask= -1L ^ (-1L << workerIdBits);
  。。。什么意思呀?它先執行的是-1L << workerIdBits,workerIdBits是5。這又是什么意思呢?注意,這是位運算,long用的是補碼,-1L,就是64個1,這里使用-1是為了格式化所有位數,<<是左移運算,-1L左移五位,低位補零,也就是左移空出來的會自動補0,於是就低位五位是0,其余是1。然后^這個符號,是異或,也是位運算,位上相同則為0,不通則為1,和-1做異或,則把所有的0和1顛倒了一下。這時候,我們再看,workerId & workerMask,與操作,兩個位上都為1的才能唯一,否則為零,workerMask高位都是0,所以,不管workerId高位是什么,都是0,;而workerMask低位都是1,所以,不管workerId低位是什么,都會被保留,於是,我們就控制了workerId的范圍。

最后的異常
  這里,時間戳,保證了不通毫秒不同,然后機器編碼進程編碼保證了不同進程不通,再然后,序列,在統一毫秒內,如果獲取第二個ID,則序列號+1,到下一毫秒后重置。至此,唯一性ok。但是,還有問題,序列號用完了怎么辦?代碼里的解決方案是,等到下一毫秒。

補充
  其實,這個方案中,機器碼和進程編碼是可能相同的,只是概率比較小,我們就湊合着用吧。如果有更好地獲取這兩位的方式,歡迎溝通。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM