如何保證 ID 的全局唯一性?
分庫分表之後如何生成全局唯一的資料庫主鍵呢?
資料庫中的主鍵如何選擇?
資料庫中的每條記錄都需要有一個唯一的辨別,根據資料庫第二範式,資料庫中每個表都需要唯一主鍵,其他元素和主鍵一一對應。
一般有兩種選擇方式:
- 使用業務字段作為主鍵,比如使用者表來說,可以使用手機号, email ,或者身份證作為主鍵。
- 使用唯一 ID 作為主鍵
如果使用唯一 ID 作為主鍵,就需要保證 ID 的全局唯一性,如何保證唯生成全局唯一性的ID ?
Snowflake 算法
Snowflake 算法思想是将 64bit 的二進制分成若幹,每部分都存儲有特定含義:41位時間戳,10位機器碼,12位序列号。

pow(2,41) 這樣一算,基本上可以使用69年了。
- 1bit:一般是符号位,不做處理
- 41bit:用來記錄時間戳,這裡可以記錄69年,如果設定好起始時間比如今年是2018年,那麼可以用到2089年,到時候怎麼辦?要是這個系統能用69年,我相信這個系統早都重構了好多次了。
- 10bit:10bit用來記錄機器ID,總共可以記錄1024台機器,一般用前5位代表資料中心,後面5位是某個資料中心的機器ID
- 12bit:循環位,用來對同一個毫秒之内産生不同的ID,12位可以最多記錄4095個,也就是在同一個機器同一毫秒最多記錄4095個,多餘的需要進行等待下毫秒。
public class SnowflakeIdWorker { /** * 雪花算法解析 結構 snowflake的結構如下(每部分用-分開): * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 * 第一位為未使用,接下來的41位為毫秒級時間(41位的長度可以使用69年),然後是5位datacenterId和5位workerId(10 * 位的長度最多支援部署1024個節點) ,最後12位是毫秒内的計數(12位的計數順序号支援每個節點每毫秒産生4096個ID序号) * * 一共加起來剛好64位,為一個Long型。(轉換成字元串長度為18) * */ // ==============================Fields=========================================== /** 開始時間截 (2015-01-01) */ private final long twepoch = 1489111610226L; /** 機器id所占的位數 */ private final long workerIdBits = 5L; /** 資料辨別id所占的位數 */ private final long dataCenterIdBits = 5L; /** 支援的最大機器id,結果是31 (這個移位算法可以很快的計算出幾位二進制數所能表示的最大十進制數) */ private final long maxWorkerId = -1L ^ (-1L << workerIdBits); /** 支援的最大資料辨別id,結果是31 */ private final long maxDataCenterId = -1L ^ (-1L << dataCenterIdBits); /** 序列在id中占的位數 */ private final long sequenceBits = 12L; /** 機器ID向左移12位 */ private final long workerIdShift = sequenceBits; /** 資料辨別id向左移17位(12+5) */ private final long dataCenterIdShift = sequenceBits + workerIdBits; /** 時間截向左移22位(5+5+12) */ private final long timestampLeftShift = sequenceBits + workerIdBits + dataCenterIdBits; /** 生成序列的掩碼,這裡為4095 (0b111111111111=0xfff=4095) */ private final long sequenceMask = -1L ^ (-1L << sequenceBits); /** 工作機器ID(0~31) */ private long workerId; /** 資料中心ID(0~31) */ private long dataCenterId; /** 毫秒内序列(0~4095) */ private long sequence = 0L; /** 上次生成ID的時間截 */ private long lastTimestamp = -1L; // ==============================Constructors===================================== /** * 構造函數 * @param workerId 工作ID (0~31) * @param dataCenterId 資料中心ID (0~31) */ public SnowflakeIdWorker(long workerId, long dataCenterId) { if (workerId > maxWorkerId || workerId < 0) { throw new IllegalArgumentException(String.format("workerId can't be greater than %d or less than 0", maxWorkerId)); } if (dataCenterId > maxDataCenterId || dataCenterId < 0) { throw new IllegalArgumentException(String.format("dataCenterId can't be greater than %d or less than 0", maxDataCenterId)); } this.workerId = workerId; this.dataCenterId = dataCenterId; } // ==============================Methods========================================== /** * 獲得下一個ID (該方法是線程安全的) * @return SnowflakeId */ public synchronized long nextId() { long timestamp = timeGen(); // 如果目前時間小于上一次ID生成的時間戳,說明系統時鐘回退過這個時候應當抛出異常 if (timestamp < lastTimestamp) { throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp)); } // 如果是同一時間生成的,則進行毫秒内序列 // sequenceMask 為啥是4095 2^12 = 4096 if (lastTimestamp == timestamp) { // 每次+1 sequence = (sequence + 1) & sequenceMask; // 毫秒内序列溢出 if (sequence == 0) { // 阻塞到下一個毫秒,獲得新的時間戳 timestamp = tilNextMillis(lastTimestamp); } } // 時間戳改變,毫秒内序列重置 else { sequence = 0L; } // 上次生成ID的時間截 lastTimestamp = timestamp; // 移位并通過或運算拼到一起組成64位的ID // 為啥時間戳減法向左移動22 位 因為 5位datacenterid // 為啥 datCenterID向左移動17位 因為 前面有5位workid 還有12位序列号 就是17位 //為啥 workerId向左移動12位 因為 前面有12位序列号 就是12位 System.out.println(((timestamp - twepoch) << timestampLeftShift) // | (dataCenterId << dataCenterIdShift) // | (workerId << workerIdShift) // | sequence); return ((timestamp - twepoch) << timestampLeftShift) // | (dataCenterId << dataCenterIdShift) // | (workerId << workerIdShift) // | sequence; } /** * 阻塞到下一個毫秒,直到獲得新的時間戳 * @param lastTimestamp 上次生成ID的時間截 * @return 目前時間戳 */ protected long tilNextMillis(long lastTimestamp) { long timestamp = timeGen(); while (timestamp <= lastTimestamp) { timestamp = timeGen(); } return timestamp; } /** * 傳回以毫秒為機關的目前時間 * @return 目前時間(毫秒) */ protected long timeGen() { return System.currentTimeMillis(); } // ==============================Test============================================= /** 測試 */ public static void main(String[] args) { System.out.println(System.currentTimeMillis()); SnowflakeIdWorker idWorker = new SnowflakeIdWorker(1, 1); long startTime = System.nanoTime(); for (int i = 0; i < 50000; i++) { long id = idWorker.nextId(); System.out.println(id); } System.out.println((System.nanoTime() - startTime) / 1000000 + "ms"); }}
Snowflake 工程化之後,會有兩種實作方式:
- 嵌入業務代碼,也就是分布在業務伺服器中,這種方案的好處是業務代碼在使用的時候不需要網絡調用,性能會比較好,但是這樣有個問題, 随着業務伺服器的數量變多,很難保證機器 ID 的唯一性。有的方案是采用 資料庫自增id ,或者 zookeeper擷取唯一的機器ID。
- 另外一個部署方式是将信号發生器作為獨立的服務部署,業務使用信号發生的時候需要多一次網絡調用,存在對内網調用性能的損耗,發号器部署執行個體是有限的,一般可以将機器 ID解除安裝配置檔案裡,這樣可以保證機器 ID的唯一性。通常單執行個體單 CPU 可以達到兩萬每秒。
snowflake 算法可能存在的問題:
依賴系統的時間戳,一旦系統時間不準,會産生重複的ID
如何解決這個問題呢?
- 時間戳不記錄毫秒而是記錄秒,通一個時間區間裡可以部署多個發号器,避免出現分庫分表時分布不均勻。
- 生成序列号可以使用随機的。
上面的方法主要是兩種思路:
- 讓算法中的ID符合規則自己的業務特點
- 解決時間回撥的問題。
歡迎關注公衆号:程式員開發者社群
本文使用 mdnice 排版