snowflake算法的workerId問題


snowflake做為一個輕量級的分布式id生成算法,已經被廣泛使用,大致原理如下:

中間10位工作機器id(即:workerId),從圖上可以知道,最多2^10次方,即1024台機器 

最右側12位序列號,2^12次方,即:4096

理論上,如果部署1024台機器,1ms內最多可生成1024*4096 = 4194304(約400萬) 個id ,大多數應用場景中已經足夠了。

根據這個思路,有很多語言版本的實現,下面是java版本:

public class SnowFlake {

    /**
     * 起始的時間戳
     */
    private final static long START_STMP = 1480166465631L;

    /**
     * 每一部分占用的位數
     */
    private final static long SEQUENCE_BIT = 12; //序列號占用的位數
    private final static long MACHINE_BIT = 10;   //機器標識占用的位數

    /**
     * 每一部分的最大值
     */
    private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
    private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);

    /**
     * 每一部分向左的位移
     */
    private final static long MACHINE_LEFT = SEQUENCE_BIT;
    private final static long TIMESTMP_LEFT = SEQUENCE_BIT + MACHINE_BIT;

    private long machineId;     //機器標識
    private long sequence = 0L; //序列號
    private long lastStmp = -1L;//上一次時間戳

    public SnowFlake(long machineId) {
        if (machineId > MAX_MACHINE_NUM || machineId < 0) {
            throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0");
        }
        this.machineId = machineId;
    }

    /**
     * 產生下一個ID
     *
     * @return
     */
    public synchronized long nextId() {
        long currStmp = getNewstmp();
        if (currStmp < lastStmp) {
            throw new RuntimeException("Clock moved backwards.  Refusing to generate id");
        }

        if (currStmp == lastStmp) {
            //相同毫秒內,序列號自增
            sequence = (sequence + 1) & MAX_SEQUENCE;
            //同一毫秒的序列數已經達到最大
            if (sequence == 0L) {
                currStmp = getNextMill();
            }
        } else {
            //不同毫秒內,序列號置為0
            sequence = 0L;
        }

        lastStmp = currStmp;

        return (currStmp - START_STMP) << TIMESTMP_LEFT //時間戳部分
                | machineId << MACHINE_LEFT             //機器標識部分
                | sequence;                             //序列號部分
    }

    private long getNextMill() {
        long mill = getNewstmp();
        while (mill <= lastStmp) {
            mill = getNewstmp();
        }
        return mill;
    }

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

    public static void main(String[] args) {
        SnowFlake snowFlake = new SnowFlake(0);

        for (int i = 0; i < (1 << 12); i++) {
            System.out.println(snowFlake.nextId());
        }

    }
}

結合前面提到的原理可知,集群部署環境下每台機器的應用啟動時,初始化SnowFlake應該指定集群內唯一的workerId,否則如果每個機器上的workerId都一樣,就有可能生成重復的id(即:相當於集群中,只有一個workerId,這樣同1ms內,最多也就生成4096個id,這在高並發業務系統中,是很容易達到的)。

 

很多朋友都知道,機器上的ip可以轉換成int數據,很容易想到,由於每台機器的ip不同(至少同1集群中不會重復),將ip轉換出來的數字,對worker上限總數取模(注:worker總數只要小於1024即可,比如假設集群部署的機器,不會超過512台,就可以指定worker總數為 512),用這個取模的結果做為workerId似乎是一個不錯的選擇(事實上有的項目就是這么干的),上線后,大概率也能平穩運行。

 

但是!現在很多項目都是跑在雲上(或k8s集群中),分布式環境中容器出現問題被重啟是不可避免的,而且機器重啟后通常ip也會變化。可能有一天會突然發現,snowflake生成的id出現了重復,但是代碼並沒有做過任何變更!

 

隱患就在於上面提到的ip取模算法,先給出ip轉換成int的方法(網上copy來的):

public class IpUtils {

    // 將127.0.0.1形式的IP地址轉換成十進制整數,這里沒有進行任何錯誤處理
    public static long ipToLong(String strIp) {
        long[] ip = new long[4];
        // 先找到IP地址字符串中.的位置
        int position1 = strIp.indexOf(".");
        int position2 = strIp.indexOf(".", position1 + 1);
        int position3 = strIp.indexOf(".", position2 + 1);
        // 將每個.之間的字符串轉換成整型
        ip[0] = Long.parseLong(strIp.substring(0, position1));
        ip[1] = Long.parseLong(strIp.substring(position1 + 1, position2));
        ip[2] = Long.parseLong(strIp.substring(position2 + 1, position3));
        ip[3] = Long.parseLong(strIp.substring(position3 + 1));
        return (ip[0] << 24) + (ip[1] << 16) + (ip[2] << 8) + ip[3];
    }

    // 將十進制整數形式轉換成127.0.0.1形式的ip地址
    public static String longToIP(long longIp) {
        StringBuffer sb = new StringBuffer("");
        // 直接右移24位
        sb.append(String.valueOf((longIp >>> 24)));
        sb.append(".");
        // 將高8位置0,然后右移16位
        sb.append(String.valueOf((longIp & 0x00FFFFFF) >>> 16));
        sb.append(".");
        // 將高16位置0,然后右移8位
        sb.append(String.valueOf((longIp & 0x0000FFFF) >>> 8));
        sb.append(".");
        // 將高24位置0
        sb.append(String.valueOf((longIp & 0x000000FF)));
        return sb.toString();
    }
}

如果worker總數最大為512,看看下面2個ip,按前面的思路,取模后的結果如何:

long p1 = IpUtils.ipToLong("10.47.130.37");
long p2 = IpUtils.ipToLong("10.96.184.37");

int workerCount = 512;

System.out.println(p1 % workerCount);
System.out.println(p2 % workerCount);

得到2個37,也就是這2台機器生成相同的workerId,所以它倆在並發高的情況下,有就較大概率生成相同的id,而且這個bug還挺難查的,可能機器一重啟,又正常了(因為ip變了),如果只是偶爾出現,還會讓人誤以為是“時鍾回撥”問題。

 

那么,合理的做法應該如何設置workerId呢?可以借助redis,對集群內的機器在應用啟動時做一個workerId的全局登記,流程圖如下:

注1:因為容器隨時可能被銷毀,如果機器沒了,登記表里的記錄就沒用了,相當於成了臟數據,所以檢查過程中,有一步清理過期記錄就是用來干這個的(判斷是否過期記錄,可借助“登記時間戳”來判斷,比如3個月前登記的認為是無效的)

注2:意外情況下,比如啟動時正好redis發生故障連不上,可以考慮降級為隨機生成1個workerId先用着(視業務場景酌情而定)

 

最后,順便提一句,如果考慮到時鍾回撥問題,可以使用一些大廠的改進版本,比如百度的uid-generator ,或美團的leaf


免責聲明!

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



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