分布式ID常見生成策略:
分布式ID生成策略常見的有如下幾種:
- 數據庫自增ID。
- UUID生成。
- Redis的原子自增方式。
- 數據庫水平拆分,設置初始值和相同的自增步長。
- 批量申請自增ID。
- 雪花算法。
- 百度UidGenerator算法(基於雪花算法實現自定義時間戳)。
- 美團Leaf算法(依賴於數據庫,ZK)。
本文主要介紹SnowFlake 算法,是 Twitter 開源的分布式 id 生成算法。
其核心思想就是:使用一個 64 bit 的 long 型的數字作為全局唯一 id。在分布式系統中的應用十分廣泛,且ID 引入了時間戳,保持自增性且不重復。
雪花算法的結構:
主要分為 5 個部分:
- 是 1 個 bit:0,這個是無意義的。
- 是 41 個 bit:表示的是時間戳。
- 是 10 個 bit:表示的是機房 id,0000000000,因為我傳進去的就是0。
- 是 12 個 bit:表示的序號,就是某個機房某台機器上這一毫秒內同時生成的 id 的序號,0000 0000 0000。
接下去我們來解釋一下四個部分:
1 bit,是無意義的:
因為二進制里第一個 bit 為如果是 1,那么都是負數,但是我們生成的 id 都是正數,所以第一個 bit 統一都是 0。
41 bit:表示的是時間戳,單位是毫秒。
41 bit 可以表示的數字多達 2^41 - 1,也就是可以標識 2 ^ 41 - 1 個毫秒值,換算成年就是表示 69 年的時間。
10 bit:記錄工作機器 id,代表的是這個服務最多可以部署在 2^10 台機器上,也就是 1024 台機器。
但是 10 bit 里 5 個 bit 代表機房 id,5 個 bit 代表機器 id。意思就是最多代表 2 ^ 5 個機房(32 個機房),每個機房里可以代表 2 ^ 5 個機器(32 台機器),這里可以隨意拆分,比如拿出4位標識業務號,其他6位作為機器號。可以隨意組合。
12 bit:這個是用來記錄同一個毫秒內產生的不同 id。
12 bit 可以代表的最大正整數是 2 ^ 12 - 1 = 4096,也就是說可以用這個 12 bit 代表的數字來區分同一個毫秒內的 4096 個不同的 id。也就是同一毫秒內同一台機器所生成的最大ID數量為4096
簡單來說,你的某個服務假設要生成一個全局唯一 id,那么就可以發送一個請求給部署了 SnowFlake 算法的系統,由這個 SnowFlake 算法系統來生成唯一 id。這個 SnowFlake 算法系統首先肯定是知道自己所在的機器號,(這里姑且講10bit全部作為工作機器ID)接着 SnowFlake 算法系統接收到這個請求之后,首先就會用二進制位運算的方式生成一個 64 bit 的 long 型 id,64 個 bit 中的第一個 bit 是無意義的。接着用當前時間戳(單位到毫秒)占用41 個 bit,然后接着 10 個 bit 設置機器 id。最后再判斷一下,當前這台機房的這台機器上這一毫秒內,這是第幾個請求,給這次生成 id 的請求累加一個序號,作為最后的 12 個 bit。
代碼實現:
代碼中將 10 bit 拆分成 5bit表示工作機器ID,5bit表示數據中心ID
public class SnowflakeIdWorker { // ==============================Fields===========================================
/** 開始時間戳 (2015-01-01) */
private final long twepoch = 1420041600000L; /** 機器id所占的位數 */
private final long workerIdBits = 5L; /** 數據標識id所占的位數 */
private final long datacenterIdBits = 5L; /** 序列在id中占的位數 */
private final long sequenceBits = 12L; /** 支持的最大機器id,結果是31 (這個移位算法可以很快的計算出幾位二進制數所能表示的最大十進制數) */
private final long maxWorkerId = -1L ^ (-1L << workerIdBits); /** 支持的最大數據標識id,結果是31 */
private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); /** 機器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("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; } // ==============================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)); } //如果是同一時間生成的,則進行毫秒內序列
if (lastTimestamp == timestamp) { sequence = (sequence + 1) & sequenceMask; //毫秒內序列溢出
if (sequence == 0) { //阻塞到下一個毫秒,獲得新的時間戳
timestamp = tilNextMillis(lastTimestamp); } } //時間戳改變,毫秒內序列重置
else { sequence = 0L; } //上次生成ID的時間戳
lastTimestamp = timestamp; //移位並通過或運算拼到一起組成64位的ID
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) { SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 0); for (int i = 0; i < 1000; i++) { long id = idWorker.nextId(); System.out.println(Long.toBinaryString(id)); System.out.println(id); } } }
SnowFlake的優點是,整體上按照時間自增排序,並且整個分布式系統內不會產生ID碰撞(由數據中心ID和機器ID作區分),並且效率較高,經測試,SnowFlake每秒能夠產生26萬ID左右。但是依賴與系統時間的一致性,如果系統時間被回調,或者改變,可能會造成id沖突或者重復。實際中我們的機房並沒有那么多,我們可以改進改算法,將10bit的機器id優化,成業務表或者和我們系統相關的業務。
其實對於分布式ID的生成策略。無論是我們上述提到的哪一種。無非需要具有以下兩種特點。 自增的、不重復的。而對於不重復且是自增的,那么我們是很容易想到的是時間,而雪花算法就是基於時間戳。但是毫秒級的並發下如果直接拿來用,顯然是不合理的。那么我們就要在這個時間戳上面做一些文章。至於怎么能讓這個東西保持唯一且自增。就要打開自己的腦洞了。可以看到雪花算法中是基於 synchronized 鎖進行實現的。如果小伙伴們有其他更好的想法請在下方留言哦。