分散式序號發生器-snowflake雪花演算法
全域性唯一ID,目的是讓分散式系統中的所有元素都能有唯一的識別資訊。
1.UUID
UUID概述
UUID(Universally Unique Identifier),通用唯一識別碼。UUID是基於當前時間、計數器(counter)和硬體標識(通常為無線網絡卡的MAC地址)等資料計算生成的。
格式 & 版本
UUID由以下幾部分的組合:
- 當前日期和時間,UUID的第一個部分與時間有關,如果你在生成一個UUID之後,過幾秒又生成一個UUID,則第一個部分不同,其餘相同。
- 時鐘序列。
- 全域性唯一的IEEE機器識別號,如果有網絡卡,從網絡卡MAC地址獲得,沒有網絡卡以其他方式獲得。
UUID 是由一組32位數的16進位制數字所構成,以連字號分隔的五組來顯示,形式為 8-4-4-4-12,總共有 36個字元(即三十二個英數字母和四個連字號)。例如:
aefbbd3a-9cc5-4655-8363-a2a43e6e6c80
xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx
數字M
的表示 UUID 版本,當前規範有5個版本,M可選值為1, 2, 3, 4, 5
;
數字N
的一至四個最高有效位(bit)表示 UUID 變體( variant ),有固定的兩位10xx
,因此N只可能取值8, 9, a, b
。
UUID版本通過M表示,當前規範有5個版本,M可選值為1, 2, 3, 4, 5
。這5個版本使用不同演算法,利用不同的資訊來產生UUID,各版本有各自優勢,適用於不同情景。具體使用的資訊
-
version 1, date-time & MAC address
基於時間的UUID通過計算當前時間戳、隨機數和節點標識:機器MAC地址得到。由於在演算法中使用了MAC地址,這個版本的UUID可以保證在全球範圍的唯一性。但與此同時,使用MAC地址會帶來安全性問題,這就是這個版本UUID受到批評的地方。同時, Version 1沒考慮過一臺機器上起了兩個程序這類的問題,也沒考慮相同時間戳的併發問題,所以嚴格的Version1沒人實現,Version1的變種有Hibernate的CustomVersionOneStrategy.java、MongoDB的ObjectId.java、Twitter的snowflake等。
-
version 2, date-time & group/user id
DCE(Distributed Computing Environment)安全的UUID和基於時間的UUID演算法相同,但會把時間戳的前4位置換為POSIX的UID或GID。這個版本的UUID在實際中較少用到。
-
version 3, MD5 hash & namespace
基於名字的UUID通過計算名字和名字空間的MD5雜湊值得到。這個版本的UUID保證了:相同名字空間中不同名字生成的UUID的唯一性;不同名字空間中的UUID的唯一性;相同名字空間中相同名字的UUID重複生成是相同的。
-
version 4, pseudo-random number
根據隨機數,或者偽隨機數生成UUID。
-
version 5, SHA-1 hash & namespace
和版本3的UUID演算法類似,只是雜湊值計算使用SHA1(Secure Hash Algorithm 1)演算法。
使用較多的是版本1和版本4,其中版本1使用當前時間戳和MAC地址資訊。版本4使用(偽)隨機數資訊,128bit中,除去版本確定的4bit和variant確定的2bit,其它122bit全部由(偽)隨機數資訊確定。若希望對給定的一個字串總是能生成相同的 UUID,使用版本3或版本5。
重複機率
Java中 UUID 使用版本4進行實現,所以由java.util.UUID類產生的 UUID,128個位元中,有122個位元是隨機產生,4個位元標識版本被使用,還有2個標識變體被使用。利用生日悖論,可計算出兩筆 UUID 擁有相同值的機率約為
p(n) ≈ 1 - e-n*n/2x
其中x
為 UUID 的取值範圍,n
為 UUID 的個數。
以下是以 x = 2122計算出n筆 UUID 後產生碰撞的機率:
n | 機率 |
---|---|
68,719,476,736 = 236 | 0.0000000000000004 (4 x 10-16) |
2,199,023,255,552 = 241 | 0.0000000000004 (4 x 10-13) |
70,368,744,177,664 = 246 | 0.0000000004 (4 x 10-10) |
產生重複 UUID 並造成錯誤的情況非常低,是故大可不必考慮此問題。
機率也與隨機數產生器的質量有關。若要避免重複機率提高,必須要使用基於密碼學上的強偽隨機數產生器來生成值才行。
UUID 是由一組32位數的16進位制數字所構成,是故 UUID 理論上的總數為1632=2128,約等於3.4 x 10123。也就是說若每納秒產生1百萬個 UUID,要花100億年才會將所有 UUID 用完。
Java實現
/**
* Static factory to retrieve a type 4 (pseudo randomly generated) UUID.
* 使用靜態工廠來獲取版本4(偽隨機數生成器)的 UUID
* The {@code UUID} is generated using a cryptographically strong pseudo
* 這個UUID生成使用了強加密的偽隨機數生成器(PRNG)
* random number generator.
*
* @return A randomly generated {@code UUID}
*/
public static UUID randomUUID() {
SecureRandom ng = Holder.numberGenerator;
byte[] randomBytes = new byte[16];
ng.nextBytes(randomBytes);
randomBytes[6] &= 0x0f; /* clear version */
randomBytes[6] |= 0x40; /* set to version 4 */
randomBytes[8] &= 0x3f; /* clear variant */
randomBytes[8] |= 0x80; /* set to IETF variant */
return new UUID(randomBytes);
}
/**
* Static factory to retrieve a type 3 (name based) {@code UUID} based on
* the specified byte array.
* 靜態工廠對版本3的實現,對於給定的字串(name)總能生成相同的UUID
* @param name
* A byte array to be used to construct a {@code UUID}
*
* @return A {@code UUID} generated from the specified array
*/
public static UUID nameUUIDFromBytes(byte[] name) {
MessageDigest md;
try {
md = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException nsae) {
throw new InternalError("MD5 not supported", nsae);
}
byte[] md5Bytes = md.digest(name);
md5Bytes[6] &= 0x0f; /* clear version */
md5Bytes[6] |= 0x30; /* set to version 3 */
md5Bytes[8] &= 0x3f; /* clear variant */
md5Bytes[8] |= 0x80; /* set to IETF variant */
return new UUID(md5Bytes);
}
生成UUID
// Java語言實現
import java.util.UUID;
public class UUIDProvider{
public static void main(String[] args) {
// 利用偽隨機數生成版本為4,變體為9的UUID
System.out.println(UUID.randomUUID());
// 對於相同的名稱空間總是生成相同的UUID,版本為3,變體為9
// 名稱空間為"xxx"時生成的UUID總是為f561aaf6-ef0b-314d-8208-bb46a4ccb3ad
System.out.println(UUID.nameUUIDFromBytes("xxx".getBytes()));
}
}
優點
- 簡單,程式碼方便。
- 生成ID效能非常好,基本不會有效能問題。本地生成,沒有網路消耗。
- 全球唯一,在遇見資料遷移,系統資料合併,或者資料庫變更等情況下,可以從容應對。
缺點
- 採用無意義字串,沒有排序,無法保證趨勢遞增。
- UUID使用字串形式儲存,資料量大時查詢效率比較低
- 儲存空間比較大,如果是海量資料庫,就需要考慮儲存量的問題。
2.雪花演算法(twitter/snowflake)
雪花演算法概述
SnowFlake 演算法,是 Twitter 開源的分散式 id 生成演算法。其核心思想就是:使用一個 64 bit 的 long 型的數字作為全域性唯一 id。在分散式系統中的應用十分廣泛,且ID 引入了時間戳,基本上保持自增的。其原始版本是scala版,後面出現了許多其他語言的版本如Java、C++等。
格式
-
1bit - 首位無效符
-
41bit - 時間戳(毫秒級)
- 41位可以表示241-1個數字;
- 241-1毫秒,換算成年就是表示 69 年的時間
-
10bit - 工作機器id
- 5bit - datacenterId機房id
- 5bit - workerId機器 id
-
12bit - 序列號
序列號,用來記錄同一個datacenterId中某一個機器上同毫秒內產生的不同id。
特點(自增、有序、適合分散式場景)
- 時間位:可以根據時間進行排序,有助於提高查詢速度。
- 機器id位:適用於分散式環境下對多節點的各個節點進行標識,可以具體根據節點數和部署情況設計劃分機器位10位長度,如劃分5位表示程序位等。
- 序列號位:是一系列的自增id,可以支援同一節點同一毫秒生成多個ID序號,12位的計數序列號支援每個節點每毫秒產生4096個ID序號
snowflake演算法可以根據專案情況以及自身需要進行一定的修改。
Twitter演算法實現
Twitter演算法實現(Scala)
Java演算法實現
public class IdWorker{
//10bit的工作機器id
private long workerId; // 5bit
private long datacenterId; // 5bit
private long sequence; // 12bit 序列號
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));
}
System.out.printf("worker starting. timestamp left shift %d, datacenter id bits %d, worker id bits %d, sequence bits %d, workerid %d",
timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId);
this.workerId = workerId;
this.datacenterId = datacenterId;
this.sequence = sequence;
}
//初始時間戳
private long twepoch = 1288834974657L;
//長度為5位
private long workerIdBits = 5L;
private long datacenterIdBits = 5L;
//最大值 -1 左移 5,得結果a,-1 異或 a:利用位運算計算出5位能表示的最大正整數是多少。
private long maxWorkerId = -1L ^ (-1L << workerIdBits); //31
private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); // 31
//序列號id長度
private long sequenceBits = 12L;
//序列號最大值
private long sequenceMask = -1L ^ (-1L << sequenceBits); //4095
//workerId需要左移的位數,12位
private long workerIdShift = sequenceBits; //12
//datacenterId需要左移位數
private long datacenterIdShift = sequenceBits + workerIdBits; // 12+5=17
//時間戳需要左移位數
private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits; // 12+5+5=22
//上次時間戳,初始值為負數
private long lastTimestamp = -1L;
public long getWorkerId(){
return workerId;
}
public long getDatacenterId(){
return datacenterId;
}
public long getTimestamp(){
return System.currentTimeMillis();
}
//下一個ID生成演算法
public synchronized long nextId() {
long timestamp = timeGen();
//獲取當前時間戳如果小於上次時間戳,則表示時間戳獲取出現異常
if (timestamp < lastTimestamp) {
System.err.printf("clock is moving backwards. Rejecting requests until %d.", lastTimestamp);
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds",
lastTimestamp - timestamp));
}
//獲取當前時間戳如果等於上次時間戳(同一毫秒內),則在序列號加一;否則序列號賦值為0,從0開始。
if (lastTimestamp == timestamp) {
// 通過位與運算保證計算的結果範圍始終是 0-4095
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0;
}
//將上次時間戳值重新整理
lastTimestamp = timestamp;
/**
* 返回結果:
* (timestamp - twepoch) << timestampLeftShift) 表示將時間戳減去初始時間戳,再左移相應位數
* (datacenterId << datacenterIdShift) 表示將資料id左移相應位數
* (workerId << workerIdShift) 表示將工作id左移相應位數
* | 是按位或運算子,例如:x | y,只有當x,y都為0的時候結果才為0,其它情況結果都為1。
* 因為個部分只有相應位上的值有意義,其它位上都是0,所以將各部分的值進行 | 運算就能得到最終拼接好的id
*/
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) {
IdWorker worker = new IdWorker(1,1,1);
for (int i = 0; i < 30; i++) {
System.out.println(worker.nextId());
}
}
}
優點
- 毫秒數在高位,自增序列在低位,整個ID都是趨勢遞增的。
- 不依賴資料庫等第三方系統,以服務的方式部署,穩定性更高,生成ID的效能也是非常高的。
- 可以根據自身業務特性分配bit位,非常靈活。
缺點
- 雪花演算法在單機系統上ID是遞增的,但是在分散式系統多節點的情況下,所有節點的時鐘並不能保證不完全同步,所以有可能會出現不是全域性遞增的情況。如果系統時間被回撥,或者改變,可能會造成id衝突或者重複。
3.利用資料庫的auto_increment特性
以MySQL舉例,利用給欄位設定auto_increment_increment和auto_increment_offset來保證ID自增,每次業務使用下列SQL讀寫MySQL得到ID號
優點
- 非常簡單,利用現有資料庫系統的功能實現,成本小,有DBA專業維護。
- ID號單調自增,可以實現一些對ID有特殊要求的業務。
缺點
- 強依賴DB,當DB異常時整個系統不可用,屬於致命問題。配置主從複製可以儘可能的增加可用性,但是資料一致性在特殊情況下難以保證。主從切換時的不一致可能會導致重複發號。
- ID發號效能瓶頸限制在單臺MySQL的讀寫效能
- 分表分庫,資料遷移合併等比較麻煩