在分布式系統中,經常需要使用全局唯一ID
查找對應的數據。產生這種ID需要保證系統全局唯一,而且要高性能以及占用相對較少的空間。
全局唯一ID在數據庫中一般會被設成主鍵,這樣為了保證數據插入時索引的快速建立,還需要保持一個有序的趨勢。
這樣全局唯一ID就需要保證這兩個需求:
當服務使用的數據庫只有單庫單表時,可以利用數據庫的auto_increment
來生成全局唯一遞增ID.
優勢:
劣勢:
一般的語言中會自帶UUID的實現,比如Java中UUID方式UUID.randomUUID().toString()
,可以通過服務程序本地產生,ID的生成不依賴數據庫的實現。
優勢:
劣勢:
snowflake
是twitter開源的分布式ID生成算法,其核心思想是:產生一個long型的ID,使用其中41bit作為毫秒數,10bit作為機器編號,12bit作為毫秒內序列號。這個算法單機每秒內理論上最多可以生成1000*(2^12)
個,也就是大約400W
的ID,完全能滿足業務的需求。
根據snowflake
算法的思想,我們可以根據自己的業務場景,產生自己的全局唯一ID。因為Java中long
類型的長度是64bits,所以我們設計的ID需要控制在64bits。
比如我們設計的ID包含以下信息:
| 41 bits: Timestamp | 3 bits: 區域 | 10 bits: 機器編號 | 10 bits: 序列號 |
產生唯一ID的Java
代碼:
import java.security.SecureRandom;
/**
* 自定義 ID 生成器
* ID 生成規則: ID長達 64 bits
*
* | 41 bits: Timestamp (毫秒) | 3 bits: 區域(機房) | 10 bits: 機器編號 | 10 bits: 序列號 |
*/
public class CustomUUID {
// 基准時間
private long twepoch = 1288834974657L; //Thu, 04 Nov 2010 01:42:54 GMT
// 區域標志位數
private final static long regionIdBits = 3L;
// 機器標識位數
private final static long workerIdBits = 10L;
// 序列號識位數
private final static long sequenceBits = 10L;
// 區域標志ID最大值
private final static long maxRegionId = -1L ^ (-1L << regionIdBits);
// 機器ID最大值
private final static long maxWorkerId = -1L ^ (-1L << workerIdBits);
// 序列號ID最大值
private final static long sequenceMask = -1L ^ (-1L << sequenceBits);
// 機器ID偏左移10位
private final static long workerIdShift = sequenceBits;
// 業務ID偏左移20位
private final static long regionIdShift = sequenceBits + workerIdBits;
// 時間毫秒左移23位
private final static long timestampLeftShift = sequenceBits + workerIdBits + regionIdBits;
private static long lastTimestamp = -1L;
private long sequence = 0L;
private final long workerId;
private final long regionId;
public CustomUUID(long workerId, long regionId) {
// 如果超出范圍就拋出異常
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException("worker Id can't be greater than %d or less than 0");
}
if (regionId > maxRegionId || regionId < 0) {
throw new IllegalArgumentException("datacenter Id can't be greater than %d or less than 0");
}
this.workerId = workerId;
this.regionId = regionId;
}
public CustomUUID(long workerId) {
// 如果超出范圍就拋出異常
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException("worker Id can't be greater than %d or less than 0");
}
this.workerId = workerId;
this.regionId = 0;
}
public long generate() {
return this.nextId(false, 0);
}
/**
* 實際產生代碼的
*
* @param isPadding
* @param busId
* @return
*/
private synchronized long nextId(boolean isPadding, long busId) {
long timestamp = timeGen();
long paddingnum = regionId;
if (isPadding) {
paddingnum = busId;
}
if (timestamp < lastTimestamp) {
try {
throw new Exception("Clock moved backwards. Refusing to generate id for " + (lastTimestamp - timestamp) + " milliseconds");
} catch (Exception e) {
e.printStackTrace();
}
}
//如果上次生成時間和當前時間相同,在同一毫秒內
if (lastTimestamp == timestamp) {
//sequence自增,因為sequence只有10bit,所以和sequenceMask相與一下,去掉高位
sequence = (sequence + 1) & sequenceMask;
//判斷是否溢出,也就是每毫秒內超過1024,當為1024時,與sequenceMask相與,sequence就等於0
if (sequence == 0) {
//自旋等待到下一毫秒
timestamp = tailNextMillis(lastTimestamp);
}
} else {
// 如果和上次生成時間不同,重置sequence,就是下一毫秒開始,sequence計數重新從0開始累加,
// 為了保證尾數隨機性更大一些,最後一位設置一個隨機數
sequence = new SecureRandom().nextInt(10);
}
lastTimestamp = timestamp;
return ((timestamp - twepoch) << timestampLeftShift) | (paddingnum << regionIdShift) | (workerId << workerIdShift) | sequence;
}
// 防止產生的時間比之前的時間還要小(由於NTP回撥等問題),保持增量的趨勢.
private long tailNextMillis(final long lastTimestamp) {
long timestamp = this.timeGen();
while (timestamp <= lastTimestamp) {
timestamp = this.timeGen();
}
return timestamp;
}
// 獲取當前的時間戳
protected long timeGen() {
return System.currentTimeMillis();
}
}
使用自定義的這種方法需要注意的幾點:
NTP時間服務器
回撥服務器的時間。CustomUUID
類,最好在一個系統中能保持單例模式運行。