1. 程式人生 > >分庫分表要解決的問題之UUID生成策略

分庫分表要解決的問題之UUID生成策略

目錄

背景

UUID的幾個生成策略

使用全域性表:每次新增的時候從全域性表中取

使用Redis:將UUID資訊存放在Redis中,每次從Redis中取

Twitter的snowflake:純Java程式碼,ID生成器


背景

要實現分庫分表,要解決的一個問題就是uuid的唯一性。比如我現在講使用者表分成了三個庫來存放,每個庫裡面都有一個使用者表。

如果在沒有分庫之前,uuid可以通過mysql資料庫層面來生成,可以保證uuid是不重複的。但是現在分為了三個庫,mysql可以實現每個庫裡面的資料不重複的,但是不能保證庫與庫之間的uuid是不重複的。

這就要我們手動指定uuid,而且要保證不同庫之間的uuuid不能重複。

UUID的幾個生成策略

使用全域性表:每次新增的時候從全域性表中取

在資料庫中維護一個全域性表,用於新增的時候取uuid用。用完將nextid加1,更新到資料庫中,以備下次使用。

結構如下圖所示:

當然缺點很明顯,所有插入都要訪問該表,該表很容易造成系統性能瓶頸。還有一個是存在單點問題,一旦該表資料庫失效,整個應用將無法工作。

使用Redis:將UUID資訊存放在Redis中,每次從Redis中取

剛才存在資料庫中的全域性表擁有的缺點,可以用redis來解決。redis快取不容易造成瓶頸,速度也很快。

Twitter的snowflake:純Java程式碼,ID生成器

本來我以為這就終點了,但是發現了第三種方法,簡單高效的ID生成器。

snowflake演算法是一款本地生成的(ID生成過程中不依賴任何中間,無網路通訊),保證ID全域性唯一,並且ID總體有序遞增,效能每秒生成300w+。

public class SnowFlake {
    // 起始的時間戳
    private final static long START_STMP = 1480166465631L;
    // 每一部分佔用的位數,就三個
    private final static long SEQUENCE_BIT = 12;// 序列號佔用的位數
    private final static long MACHINE_BIT = 5; // 機器標識佔用的位數
    private final static long DATACENTER_BIT = 5;// 資料中心佔用的位數
    // 每一部分最大值
    private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT);
    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 DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
    private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT;
    private long datacenterId; // 資料中心
    private long machineId; // 機器標識
    private long sequence = 0L; // 序列號
    private long lastStmp = -1L;// 上一次時間戳

    public SnowFlake(long datacenterId, long machineId) {
        if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) {
            throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0");
        }
        if (machineId > MAX_MACHINE_NUM || machineId < 0) {
            throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0");
        }
        this.datacenterId = datacenterId;
        this.machineId = machineId;
    }
    //產生下一個ID
    public synchronized long nextId() {
        long currStmp = getNewstmp();
        if (currStmp < lastStmp) {
            throw new RuntimeException("Clock moved backwards.  Refusing to generate id");
        }

        if (currStmp == lastStmp) {
            //if條件裡表示當前呼叫和上一次呼叫落在了相同毫秒內,只能通過第三部分,序列號自增來判斷為唯一,所以+1.
            sequence = (sequence + 1) & MAX_SEQUENCE;
            //同一毫秒的序列數已經達到最大,只能等待下一個毫秒
            if (sequence == 0L) {
                currStmp = getNextMill();
            }
        } else {
            //不同毫秒內,序列號置為0
            //執行到這個分支的前提是currTimestamp > lastTimestamp,說明本次呼叫跟上次呼叫對比,已經不再同一個毫秒內了,這個時候序號可以重新回置0了。
            sequence = 0L;
        }

        lastStmp = currStmp;
         //就是用相對毫秒數、機器ID和自增序號拼接
        return (currStmp - START_STMP) << TIMESTMP_LEFT //時間戳部分
                | datacenterId << DATACENTER_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 class Test {

    public static void main(String[] args) {
        // 構造方法設定機器碼:第9個機房的第20臺機器
        SnowFlake  snowFlake = new SnowFlake(9, 20);
        for(int i =0; i <(1<< 12); i++){
            System.out.println(snowFlake.nextId());
        }
    }

}

演算法原理:

snowflake生成的ID是一位18位的long型資料,二進位制結構表示如下(每部分用-分開):

0 - 00000000 00000000 00000000 00000000 00000000 0 - 00000 - 00000 - 00000000 0000

第一位未使用,接下來的41位為毫秒級時間(41位的長度可以使用69年,從1970-01-01 08:00:00),然後是5位datacenterId(最大支援2^5=32個,二進位制表示從00000-11111,也即是十進位制0-31),和5位workerId(最大支援2^5=32個,原理同datacenterId),所以datacenterId*workerId最多支援部署1024個節點,最後12位是毫秒內的計數(12位的計數順序號支援每個節點每毫秒產生2^12=4096個ID序號).

所有位數加起來共64位,恰好是一個Long型(轉換為字串長度為18).

單臺機器例項,通過時間戳保證前41位是唯一的,分散式系統多臺機器例項下,通過對每個機器例項分配不同的datacenterId和workerId避免中間的10位碰撞。最後12位每毫秒從0遞增生產ID,再提一次:每毫秒最多生成4096個ID,每秒可達4096000個。理論上,只要CPU計算能力足夠,單機每秒可生產400多萬個,實測300w+,效率之高由此可見。