SnowFlake全域性唯一ID及工具類
阿新 • • 發佈:2018-12-21
正經學徒,佛系記錄,不搞事情
一、什麼是SnowFlake
twitter 用於生成id的演算法
真面目:64位的二進位制
- 1位,不用。二進位制中最高位為1的都是負數,但是我們生成的id一般都使用整數,所以這個最高位固定是0
- 41位,用來記錄時間戳(毫秒)。 41位可以表示241−1個數字, 如果只用來表示正整數(計算機中正數包含0),可以表示的數值範圍是:0 至 241−1,減1是因為可表示的數值範圍是從0開始算的,而不是1。 也就是說41位可以表示241−1個毫秒的值,轉化成單位年則是(241−1)/(1000∗60∗60∗24∗365)=69年
- 10位,用來記錄工作機器id。 可以部署在210=1024個節點,包括5位datacenterId和5位workerId 5位(bit)可以表示的最大正整數是25−1=31,即可以用0、1、2、3、....31這32個數字,來表示不同的datecenterId或workerId
- 12位,序列號,用來記錄同毫秒內產生的不同id。 12位(bit)可以表示的最大正整數是212−1=4095,即可以用0、1、2、3、....4094這4095個數字,來表示同一機器同一時間截(毫秒)內產生的4095個ID序號 SnowFlake可以保證: 所有生成的id按時間趨勢遞增 整個分散式系統內不會產生重複id(因為有datacenterId和workerId來做區分)
二、為什麼用SnowFlake
對於mysql而言,InnoDB為聚集主鍵型別的引擎,資料會按照主鍵進行排序,由於UUID的無序性,InnoDB會產生巨大的IO壓力。InnoDB主鍵索引和資料儲存位置相關(簇類索引),uuid 主鍵可能會引起資料位置頻繁變動,嚴重影響效能,而雪花演算法的高位使用的是時間,因此保證了生成的ID的大小是遞增的,因此推薦使用雪花演算法。
mysql的首要推薦當然還是使用ID自增,但是這種做法不適合使用在分散式上,同時也有人覺得只用遞增會暴露業務資訊(比如通過ID判斷產品的銷量)
三、怎麼使用
注意:如下雪花演算法最終生成的字串長度是19位。使用時直接呼叫 getId 方法。
下面的工具類作用於單個服務節點,所以workerId和datacenterId都設為0。如果有多個機器節點則建議使用配置統一管理
/** * 全域性唯一id生成工具類 */ public class SnowFlakeUtil { private long workerId; private long datacenterId; private long sequence = 0L; private long twepoch = 1288834974657L; // Thu, 04 Nov 2010 01:42:54 GMT 標記時間 用來計算偏移量,距離當前時間不同,得到的資料的位數也不同 private long workerIdBits = 5L; // 物理節點ID長度 private long datacenterIdBits = 5L; // 資料中心ID長度 private long maxWorkerId = -1L ^ (-1L << workerIdBits); // 最大支援機器節點數0~31,一共32個 private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); // 最大支援資料中心節點數0~31,一共32個 private long sequenceBits = 12L; // 序列號12位, 4095,同毫秒內生成不同id的最大個數 private long workerIdShift = sequenceBits; // 機器節點左移12位 private long datacenterIdShift = sequenceBits + workerIdBits; // 資料中心節點左移17位 private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits; // 時間毫秒數左移22位 private long sequenceMask = -1L ^ (-1L << sequenceBits); // 用於和當前時間戳做比較,以獲取最新時間 private long lastTimestamp = -1L; //成員類,SnowFlakeUtil的例項物件的儲存域 private static class IdGenHolder { private static final SnowFlakeUtil instance = new SnowFlakeUtil(); } //外部呼叫獲取SnowFlakeUtil的例項物件,確保不可變 public static SnowFlakeUtil get(){ return IdGenHolder.instance; } //初始化構造,無參構造有參函式,預設節點都是0 public SnowFlakeUtil() { this(0L, 0L); } //設定機器節點和資料中心節點數,都是 0-31 public SnowFlakeUtil(long workerId, long datacenterId) { if (workerId > maxWorkerId || workerId < 0) { throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId)); } if (datacenterId > maxDatacenterId || datacenterId < 0) { throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId)); } this.workerId = workerId; this.datacenterId = datacenterId; } //執行緒安全的id生成方法 @SuppressWarnings("all") public synchronized long nextId() { //獲取當前毫秒數 long timestamp = timeGen(); //如果伺服器時間有問題(時鐘後退) 報錯。 if (timestamp < lastTimestamp) { throw new RuntimeException(String.format( "Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp)); } //如果上次生成時間和當前時間相同,在同一毫秒內 if (lastTimestamp == timestamp) { //sequence自增,因為sequence只有12bit,所以和sequenceMask相與一下,去掉高位 sequence = (sequence + 1) & sequenceMask; //判斷是否溢位,也就是每毫秒內超過4095,當為4096時,與sequenceMask相與,sequence就等於0 if (sequence == 0) { //自旋等待到下一毫秒 timestamp = tilNextMillis(lastTimestamp); } } else { //如果和上次生成時間不同,重置sequence,就是下一毫秒開始,sequence計數重新從0開始累加,每個毫秒時間內,都是從0開始計數,最大4095 sequence = 0L; } lastTimestamp = timestamp; // 最後按照規則拼出ID 64位 // 000000000000000000000000000000000000000000 00000 00000 000000000000 //1位固定整數 time datacenterId workerId sequence return ((timestamp - twepoch) << timestampLeftShift) | (datacenterId << datacenterIdShift) | (workerId << workerIdShift) | sequence; } //比較當前時間和過去時間,防止時鐘回退(機器問題),保證給的都是最新時間/最大時間 protected long tilNextMillis(long lastTimestamp) { long timestamp = timeGen(); while (timestamp <= lastTimestamp) { timestamp = timeGen(); } return timestamp; } //獲取當前的時間戳(毫秒) protected long timeGen() { return System.currentTimeMillis(); } /** * 獲取全域性唯一編碼 */ public static String getId(){ Long id = SnowFlakeUtil.get().nextId(); return id.toString(); } }