分佈式id生成算法(SnowFlake算法)
分佈式id生成算法的有很多種,Twitter的SnowFlake就是其中經典的一種。
概述
SnowFlake算法生成id的結果是一個64bit大小的整數,它的結構如下圖:
- 1位,不用。二進制中最高位爲1的都是負數,但是我們生成的id一般都使用整數,所以這個最高位固定是0。
- 41位,用來記錄時間戳(毫秒)。
- 41位可以表示241−1個數字,如果只用來表示正整數(計算機中正數包含0),可以表示的數值範圍是:0至241−1,減1是因爲可表示的數值範圍是從0開始算的,而不是1。也就是說41位可以表示241−1個毫秒的值,轉化成單位年則是(2^41−1)/(1000∗60∗60∗24∗365)=69年。
- 10位,用來記錄工作機器id。(此處可根據需要調整datacenterId和workerIdde位數,位數和爲10即可)。
- 可以部署在210=1024個節點,包括5位datacenterId(機房id)5位workerId(機器id)。
- 5位(bit)可以表示的最大正整數是25−1=31,即可以用0、1、2、3、…31這32個數字,來表示不同的datecenterId或workerId。
- 可以部署在210=1024個節點,包括5位datacenterId(機房id)5位workerId(機器id)。
- 12位,序列號,用來記錄同毫秒內產生的不同id。
- 12位(bit)可以表示的最大正整數是212−1=4095,即可以用0、1、2、3、…4094這4095個數字,來表示同一機器同一時間截(毫秒)內產生的4095個ID序號。
- 12位(bit)可以表示的最大正整數是212−1=4095,即可以用0、1、2、3、…4094這4095個數字,來表示同一機器同一時間截(毫秒)內產生的4095個ID序號。
由於在Java中64bit的整數是long類型,所以在Java中SnowFlake算法生成的id就是long來存儲的。
SnowFlake可以保證:
- 所有生成的id按時間趨勢遞增。
- 整個分佈式系統內不會產生重複id(因爲有datacenterId和workerId來做區分)。
Java代碼實現
package com.fyj.util;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @author fanyongju
* @Title: IdWorker
* @Description: TODO
* @date 2018/10/1219:35
*/
public class IdWorker {
private long workerId;
private long datacenterId;
private long sequence;
public IdWorker(long workerId, long datacenterId, long sequence) {
// sanity check for workerId
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;
this.sequence = sequence;
}
private long twepoch = 1539920215000L;//初始毫秒時間戳2018-10-19 11:36:55
private long workerIdBits = 7L;//機器選擇2^7=128(每個機房最多128臺機器)機器id爲0~127
private long datacenterIdBits = 3L;//機房選擇2^3=8(最多8個機房)機房id爲0~7
private long maxWorkerId = -1L ^ (-1L << workerIdBits);//^異或:兩個比較的位不同時其結果是1,相同結果爲0
private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
private long sequenceBits = 12L;
private long workerIdShift = sequenceBits;
private long datacenterIdShift = sequenceBits + workerIdBits;
private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
private long sequenceMask = -1L ^ (-1L << sequenceBits);
private long lastTimestamp = -1L;
public long getWorkerId() {
return workerId;
}
public long getDatacenterId() {
return datacenterId;
}
public long getTimestamp() {
return System.currentTimeMillis();
}
//用同步鎖保證線程安全
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;
//sequence等於0說明毫秒內序列已經增長到最大值
if (sequence == 0) {
//阻塞到下一個毫秒,獲得新的時間戳
timestamp = tilNextMillis(lastTimestamp);
}
} else {//時間戳改變,毫秒內序列重置
sequence = 0;//這種重置可能會導致id分佈不均,後期如果需要的話,建議改爲0-4095之間的隨機數
}
//記錄上次生成ID的時間截
lastTimestamp = timestamp;
/*
移位並通過或運算拼到一起組成64位的ID,將timestamp減去指定的初始時間戳twepoch結果左移timestampLeftShift(22)位
,然後與上左移datacenterIdShift(19)的datacenterId,再與上左移workerIdShift(12)位的workerId,再與上sequence。
注意:此處轉換成二進制進行操作
*/
return ((timestamp - twepoch) << timestampLeftShift) |
(datacenterId << datacenterIdShift) |
(workerId << workerIdShift) |
sequence;
}
private long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
private long timeGen() {
return System.currentTimeMillis();
}
public static void main(String[] args) {//通過生成的id反解出dataCenId和workId
IdWorker idWorker = new IdWorker(39, 7, 0);
while (true) {//請手動終止
Long id = idWorker.nextId();
System.out.println("generate id is:" + id);
StringBuilder dw = new StringBuilder(Long.toBinaryString(id / 4096 % 1024));
StringBuilder time = new StringBuilder(Long.toBinaryString(id / (4096 * 1024)));
if (dw.length() < 10) {
int l = 10 - dw.length();
for (int i = 0; i < l; i++) {
dw.insert(0, "0");
}
}
String dataCenterId = dw.substring(0, 3);
String workId = dw.substring(3);
Date date = new Date((Long.parseLong(time.toString(), 2) + 1288834974657L));
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
System.out.println("time:" + sdf.format(date)
+ ", dataCenterId:" + Long.parseLong(dataCenterId, 2)
+ ", workId:" + Long.parseLong(workId, 2)
+ ", sequence:" + Long.parseLong(Long.toBinaryString(id % 4096), 2));
}
}
}
代碼解釋
- 獲得單一機器的下一個序列號,使用Synchronized控制併發,而非CAS的方式,是因爲CAS不適合併發量非常高的場景。
- 如果當前毫秒在一臺機器的序列號已經增長到最大值4095,則使用while循環等待直到下一毫秒。
- 如果當前時間小於記錄的上一個毫秒值,則說明這臺機器的時間回撥了,拋出異常。但如果這臺機器的系統時間在啓動之前回撥過,那麼有可能出現ID重複的危險。
SnowFlake算法的優點:
- 生成ID時不依賴於DB,完全在內存生成,高性能高可用。
- ID呈趨勢遞增,後續插入索引樹的時候性能較好。
SnowFlake算法的缺點:
- 依賴於系統時鐘的一致性。如果某臺機器的系統時鐘回撥,有可能造成ID衝突,或者ID亂序。