推薦使用 Twitter 公司開源的 snowflake 算法。
一、分佈式ID
在複雜分佈式系統中,往往需要對大量的數據和消息進行唯一標識。比如在金融、電商、支付、等產品的系統中,數據日漸增長,對數據分庫分表後需要有一個唯一ID來標識一條數據或消息,數據庫的自增ID顯然不能滿足需求,此時一個能夠生成全局唯一ID的系統是非常必要的。
分佈式id的特點
- 全局性唯一:不能出現重複的ID號,既然是唯一標識,這是最基本的要求。
- 單調遞增:多數RDBMS使用B-tree的數據結構來存儲索引數據,在主鍵的選擇上面應該儘量使用有序的主鍵保證寫入性能。
- 支持高性能:除了對分佈式ID碼自身的要求,分佈式ID生成需支持高QPS,高可用。
- 信息安全:如果ID是連續的,惡意用戶的抓取工作就非常簡單了,直接按照順序下載指定URL即可;如果是訂單號就更危險了,競對可以直接知道一天的單量。所以在一些應用場景下,會需要ID無規則、不規則。
二、分佈式ID生成方案
- Sequence ID
- UUID
- snowflake算法(着重介紹)
三、Sequence ID
Sequence ID 是數據庫自增長序列或字段,最常見的方式。由數據庫維護,數據庫唯一。
1、優點
- 簡單,代碼方便,性能可以接受。
- 數字ID天然排序,對分頁或者需要排序的結果很有幫助。
- ID號單調自增,可以實現一些對ID有特殊要求的業務。
2、缺點
- 不同數據庫語法和實現不同,數據庫遷移的時候或多數據庫版本支持的時候需要處理。
- 在單個數據庫或讀寫分離或一主多從的情況下,只有一個主庫可以生成。有單點故障的風險。
- 在性能達不到要求的情況下,比較難於擴展。
- 強依賴DB,當DB異常時整個系統不可用,屬於致命問題。配置主從複製可以儘可能的增加可用性,但是數據一致性在特殊情況下難以保證。主從切換時的不一致可能會導致重複發號。
四、UUID
UUID 是通用唯一識別碼(Universally Unique Identifier)的縮寫,開放軟件基金會(OSF)規範定義了包括網卡MAC地址、時間戳、名字空間(Namespace)、隨機或僞隨機數、時序等元素。利用這些元素來生成UUID。
UUID是由128位二進制組成,一般轉換成十六進制,然後用String表示。在 java 的 UUID 工具類(java.util.UUID)註釋中可以看見有4種不同的UUID的生成策略:
*
* <p> The version field holds a value that describes the type of this {@code
* UUID}. There are four different basic types of UUIDs: time-based, DCE
* security, name-based, and randomly generated UUIDs. These types have a
* version value of 1, 2, 3 and 4, respectively.
*
- randomly : 基於隨機數生成UUID,由於Java中的隨機數是僞隨機數,其重複的概率是可以被計算出來的。
- time-based : 基於時間的UUID,這個一般是通過當前時間,隨機數,和本地Mac地址來計算出來,自帶的JDK包並沒有這個算法的。
- DCE security : DCE安全的UUID。
- name-based : 基於名字的UUID,通過計算名字和名字空間的MD5來計算UUID。
優點
性能非常高:本地生成,沒有網絡消耗。
缺點
- 不易於存儲:UUID太長,16字節128位,通常以36長度的字符串表示,很多場景不適用。
- 信息不安全:基於MAC地址生成UUID的算法可能會造成MAC地址泄露,這個漏洞曾被用於尋找梅麗莎病毒的製作者位置。
- ID作爲主鍵時在特定的環境會存在一些問題,比如做DB主鍵的場景下,UUID就非常不適用:MySQL官方有明確的建議主鍵要儘量越短越好,36個字符長度的UUID不符合要求。
適用場景
UUID的適用場景可以爲不需要擔心過多的空間佔用,以及不需要生成有遞增趨勢的數字。在Log4j裏面他在UuidPatternConverter中加入了UUID來標識每一條日誌。鏈路ID可以用。
五、snowflake算法
snowflake是Twitter開源的分佈式ID生成算法,結果是一個long型的ID。其核心思想是:使用41bit作爲毫秒數,10bit作爲機器的ID(5個bit是數據中心,5個bit的機器ID),12bit作爲毫秒內的流水號(意味着每個節點在每毫秒可以產生 4096 個 ID),最後還有一個符號位,永遠是0。
snowflake 算法生成的ID結構 :
- 1位標識,由於long基本類型在Java中是帶符號的,最高位是符號位,正數是0,負數是1,所以id一般是正數,最高位是0
- 41位時間截(毫秒級),注意,41位時間截不是存儲當前時間的時間截,而是存儲時間截的差值(當前時間截 - 開始時間截)得到的值),這裏的的開始時間截,一般是id生成器開始使用的時間,由程序來指定的。41位的時間截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69
- 10位的數據機器位,可以部署在1024個節點,包括5位 datacenterId 和5位 workerId
- 12位序列,毫秒內的計數,12位的計數順序號支持每個節點每毫秒(同一機器,同一時間截)產生4096個ID序號
優點
- 生成ID時不依賴於DB,完全在內存生成,高性能高可用。
- 整體上按照時間自增排序,ID呈趨勢遞增,後續插入索引樹的時候性能較好。
- 整個分佈式系統內不會產生ID碰撞(由數據中心ID和機器ID作區分)
- 效率較高
缺點
在單機上是遞增的,但是由於涉及到分佈式環境,每臺機器上的時鐘不可能完全同步,也許有時候也會出現不是全局遞增的情況。
如果某臺機器的系統時鐘回撥,有可能造成ID衝突,或者ID亂序
六、Java 實現 snowflake 算法
import lombok.extern.slf4j.Slf4j;
/**
* java 實現 snowflake 算法
*
* @author xiaohe
* @version V1.0.0
*/
@Slf4j
public class JavaDemo {
/**
* 開始時間截 (2019-08-06)
*/
private static final long TWEPOCH = 1565020800000L;
/**
* 機器id所佔的位數
*/
private static final long WORKER_ID_BITS = 5L;
/**
* 數據標識id所佔的位數
*/
private static final long DATA_CENTER_ID_BITS = 5L;
/**
* 支持的最大機器id,0~31,一共32個 (這個移位算法可以很快的計算出幾位二進制數所能表示的最大十進制數)
*/
private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);
/**
* 支持的最大數據標識id,結果是31
*/
private static final long MAX_DATA_CENTER_ID = ~(-1L << DATA_CENTER_ID_BITS);
/**
* 序列在id中佔的位數
*/
private static final long SEQUENCE_BITS = 12L;
/**
* 機器ID向左移12位
*/
private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;
/**
* 數據標識id向左移17位(12+5)
*/
private static final long DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
/**
* 時間截向左移22位(5+5+12)
*/
private static final long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATA_CENTER_ID_BITS;
/**
* 生成序列的掩碼,這裏爲4095 (0b111111111111=0xfff=4095)
*/
private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);
/**
* 工作機器ID(0~31)
*/
private long workerId;
/**
* 數據中心ID(0~31)
*/
private long dataCenterId;
/**
* 毫秒內序列(0~4095)
*/
private long sequence = 0L;
/**
* 上次生成ID的時間截
*/
private long lastTimestamp = -1L;
/**
* 構造函數
*
* @param workerId 工作ID (0~31)
* @param dataCenterId 數據中心ID (0~31)
*/
private JavaDemo(long workerId, long dataCenterId) {
if (workerId > MAX_WORKER_ID || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", MAX_WORKER_ID));
}
if (dataCenterId > MAX_DATA_CENTER_ID || dataCenterId < 0) {
throw new IllegalArgumentException(String.format("dataCenterId Id can't be greater than %d or less than 0", MAX_DATA_CENTER_ID));
}
this.workerId = workerId;
this.dataCenterId = dataCenterId;
}
/**
* 獲得下一個ID (該方法是線程安全的)
*
* @return SnowflakeId
*/
private 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) & SEQUENCE_MASK;
//毫秒內序列溢出
if (sequence == 0) {
//阻塞到下一個毫秒,獲得新的時間戳
timestamp = tilNextMillis(lastTimestamp);
}
}
//時間戳改變,毫秒內序列重置
else {
sequence = 0L;
}
//上次生成ID的時間截
lastTimestamp = timestamp;
//移位並通過或運算拼到一起組成64位的ID
return ((timestamp - TWEPOCH) << TIMESTAMP_LEFT_SHIFT)
| (dataCenterId << DATA_CENTER_ID_SHIFT)
| (workerId << WORKER_ID_SHIFT)
| sequence;
}
/**
* 阻塞到下一個毫秒,直到獲得新的時間戳
*
* @param lastTimestamp 上次生成ID的時間截
*
* @return 當前時間戳
*/
private long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
/**
* 返回以毫秒爲單位的當前時間
*
* @return 當前時間(毫秒)
*/
private long timeGen() {
return System.currentTimeMillis();
}
/**
* 測試
*/
public static void main(String[] args) {
JavaDemo idWorker = new JavaDemo(0, 0);
int loopTime = 1000;
for (int i = 0; i < loopTime; i++) {
long id = idWorker.nextId();
log.info("binary id : [{}]", Long.toBinaryString(id));
log.info("id : [{}]", id);
}
}
}
七、maven 工具實現 snowflake 算法
maven 中加入依賴:
<!-- https://mvnrepository.com/artifact/cn.hutool/hutool-captcha -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-captcha</artifactId>
<version>4.6.7</version>
</dependency>
使用
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.lang.ObjectId;
import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.net.NetUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.RandomUtil;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.PostConstruct;
/**
* ID 生成器
*
* @author xiaohe
* @version V1.0.0
*/
@Slf4j
public class IdGenerator {
private long workerId = 0;
@PostConstruct
void init() {
try {
workerId = NetUtil.ipv4ToLong(NetUtil.getLocalhostStr());
log.info("當前機器 workerId: {}", workerId);
} catch (Exception e) {
log.warn("獲取機器 ID 失敗", e);
workerId = NetUtil.getLocalhost().hashCode();
log.info("當前機器 workerId: {}", workerId);
}
}
/**
* 獲取一個批次號,形如 2019071015301361000101237
* <p>
* 數據庫使用 char(25) 存儲
*
* @param tenantId 租戶ID,5 位
* @param module 業務模塊ID,2 位
*
* @return 返回批次號
*/
public synchronized String batchId(int tenantId, int module) {
String prefix = DateTime.now().toString(DatePattern.PURE_DATETIME_MS_PATTERN);
return prefix + tenantId + module + RandomUtil.randomNumbers(3);
}
@Deprecated
public synchronized String getBatchId(int tenantId, int module) {
return batchId(tenantId, module);
}
/**
* 生成的是不帶-的字符串,類似於:b17f24ff026d40949c85a24f4f375d42
*
* @return
*/
public String simpleUUID() {
return IdUtil.simpleUUID();
}
/**
* 生成的UUID是帶-的字符串,類似於:a5c8a5e8-df2b-4706-bea4-08d0939410e3
*
* @return
*/
public String randomUUID() {
return IdUtil.randomUUID();
}
private Snowflake snowflake = IdUtil.createSnowflake(workerId, 1);
public synchronized long snowflakeId() {
return snowflake.nextId();
}
public synchronized long snowflakeId(long workerId, long dataCenterId) {
Snowflake snowflake = IdUtil.createSnowflake(workerId, dataCenterId);
return snowflake.nextId();
}
/**
* 生成類似:5b9e306a4df4f8c54a39fb0c
* <p>
* ObjectId 是 MongoDB 數據庫的一種唯一 ID 生成策略,
* 是 UUID version1 的變種,詳細介紹可見:服務化框架-分佈式 Unique ID 的生成方法一覽。
*
* @return
*/
public String objectId() {
return ObjectId.next();
}
}
github地址:https://github.com/ChaseDreamBoy/generate-distributed-id-demo