分庫分表要解決的問題之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+,效率之高由此可見。