靜態內部類單例模式實現雪花算法
在生成表主鍵ID時,我們可以考慮主鍵自增 或者 UUID,但它們都有很明顯的缺點
主鍵自增
:1、自增ID容易被爬蟲遍歷數據。2、分表分庫會有ID沖突。
UUID
: 1、太長,並且有索引碎片,索引多占用空間的問題 2、無序。
雪花算法就很適合在分布式場景下生成唯一ID,它既可以保證唯一又可以排序。為了提高生產雪花ID的效率,
在這里面數據的運算都采用的是位運算,如果對位運算不了解可以參考博客:【java提高】(17)---Java 位運算符
一、概念
1、原理
SnowFlake算法生成ID的結果是一個64bit大小的整數,它的結構如下圖:
算法描述:
-
1bit 因為二進制中最高位是符號位,1表示負數,0表示正數。生成的ID都是正整數,所以最高位固定為0。
-
41bit-時間戳 精確到毫秒級,41位的長度可以使用69年。時間位還有一個很重要的作用是可以根據時間進行排序。
-
10bit-工作機器id 10位的機器標識,10位的長度最多支持部署1024個節點。
-
12bit-序列號 序列號即一系列的自增id,可以支持同一節點同一毫秒生成多個ID序號。
12位(bit)可以表示的最大正整數是,即可以用0、1、2、3、....4094這4095個數字,來表示同一機器同一時間截(毫秒)內產生的4095個ID序號。
說明
由於在Java中64bit的整數是long類型,所以在Java中SnowFlake算法生成的id就是long來存儲的。
二、靜態類部類單例模式生產雪花ID代碼
下面生成雪花ID的代碼可以用於線上分布式項目中來生成分布式主鍵ID,因為設計采用的靜態內部類的單例模式,通過加synchronized
鎖來保證在
同一個服務器線程安全。至於不同服務器其實是不相關的,因為它們的機器碼是不一致的,所以就算同一時刻兩台服務器都產生了雪花ID,那也不會一樣的。
1、代碼
/**
* @author xub
* @Description: 雪花算法
* @date 2019/8/14 下午8:22
*/
@Slf4j
public class SnowIdUtils {
/**
* 私有的 靜態內部類
*/
private static class SnowFlake {
/**
* 內部類對象(單例模式)
*/
private static final SnowIdUtils.SnowFlake SNOW_FLAKE = new SnowIdUtils.SnowFlake();
/**
* 起始的時間戳
*/
private final long START_TIMESTAMP = 1557489395327L;
/**
* 序列號占用位數
*/
private final long SEQUENCE_BIT = 12;
/**
* 機器標識占用位數
*/
private final long MACHINE_BIT = 10;
/**
* 時間戳位移位數
*/
private final long TIMESTAMP_LEFT = SEQUENCE_BIT + MACHINE_BIT;
/**
* 最大序列號 (4095)
*/
private final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BIT);
/**
* 最大機器編號 (1023)
*/
private final long MAX_MACHINE_ID = ~(-1L << MACHINE_BIT);
/**
* 生成id機器標識部分
*/
private long machineIdPart;
/**
* 序列號
*/
private long sequence = 0L;
/**
* 上一次時間戳
*/
private long lastStamp = -1L;
/**
* 構造函數初始化機器編碼
*/
private SnowFlake() {
//模擬這里獲得本機機器編碼
long localIp = 4321;
//localIp & MAX_MACHINE_ID最大不會超過1023,在左位移12位
machineIdPart = (localIp & MAX_MACHINE_ID) << SEQUENCE_BIT;
}
/**
* 獲取雪花ID
*/
public synchronized long nextId() {
long currentStamp = timeGen();
//避免機器時鍾回撥
while (currentStamp < lastStamp) {
// //服務器時鍾被調整了,ID生成器停止服務.
throw new RuntimeException(String.format("時鍾已經回撥. Refusing to generate id for %d milliseconds", lastStamp - currentStamp));
}
if (currentStamp == lastStamp) {
// 每次+1
sequence = (sequence + 1) & MAX_SEQUENCE;
// 毫秒內序列溢出
if (sequence == 0) {
// 阻塞到下一個毫秒,獲得新的時間戳
currentStamp = getNextMill();
}
} else {
//不同毫秒內,序列號置0
sequence = 0L;
}
lastStamp = currentStamp;
//時間戳部分+機器標識部分+序列號部分
return (currentStamp - START_TIMESTAMP) << TIMESTAMP_LEFT | machineIdPart | sequence;
}
/**
* 阻塞到下一個毫秒,直到獲得新的時間戳
*/
private long getNextMill() {
long mill = timeGen();
//
while (mill <= lastStamp) {
mill = timeGen();
}
return mill;
}
/**
* 返回以毫秒為單位的當前時間
*/
protected long timeGen() {
return System.currentTimeMillis();
}
}
/**
* 獲取long類型雪花ID
*/
public static long uniqueLong() {
return SnowIdUtils.SnowFlake.SNOW_FLAKE.nextId();
}
/**
* 獲取String類型雪花ID
*/
public static String uniqueLongHex() {
return String.format("%016x", uniqueLong());
}
/**
* 測試
*/
public static void main(String[] args) throws InterruptedException {
//計時開始時間
long start = System.currentTimeMillis();
//讓100個線程同時進行
final CountDownLatch latch = new CountDownLatch(100);
//判斷生成的20萬條記錄是否有重復記錄
final Map<Long, Integer> map = new ConcurrentHashMap();
for (int i = 0; i < 100; i++) {
//創建100個線程
new Thread(() -> {
for (int s = 0; s < 2000; s++) {
long snowID = SnowIdUtils.uniqueLong();
log.info("生成雪花ID={}",snowID);
Integer put = map.put(snowID, 1);
if (put != null) {
throw new RuntimeException("主鍵重復");
}
}
latch.countDown();
}).start();
}
//讓上面100個線程執行結束后,在走下面輸出信息
latch.await();
log.info("生成20萬條雪花ID總用時={}", System.currentTimeMillis() - start);
}
}
2、測試結果
從圖中我們可以得出
1、在100個線程並發下,生成20萬條雪花ID的時間大概在1.6秒左右,所有所性能還是蠻ok的。
2、生成20萬條雪花ID並沒有一條相同的ID,因為有一條就會拋出異常了。
3、為什么說41位時間戳最長只能有69年
我們思考41的二進制,最大值也就41位都是1,也就是也就是說41位可以表示個毫秒的值,轉化成單位年則是
年
我們可以通過代碼泡一下就知道了。
public static void main(String[] args) {
//41位二進制最小值
String minTimeStampStr = "00000000000000000000000000000000000000000";
//41位二進制最大值
String maxTimeStampStr = "11111111111111111111111111111111111111111";
//轉10進制
long minTimeStamp = new BigInteger(minTimeStampStr, 2).longValue();
long maxTimeStamp = new BigInteger(maxTimeStampStr, 2).longValue();
//一年總共多少毫秒
long oneYearMills = 1L * 1000 * 60 * 60 * 24 * 365;
//算出最大可以多少年
System.out.println((maxTimeStamp - minTimeStamp) / oneYearMills);
}
運行結果
所以說雪花算法生成的ID,只能保證69年內不會重復,如果超過69年的話,那就考慮換個服務器部署吧,並且要保證該服務器的ID和之前都沒有重復過。
我相信,無論今后的道路多么坎坷,只要抓住今天,遲早會在奮斗中嘗到人生的甘甜。抓住人生中的一分一秒,勝過虛度中的一月一年!(5)