全局唯一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的讀寫性能
- 分表分庫,數據遷移合並等比較麻煩
4.Redis的INCR
當使用數據庫來生成ID性能不夠要求的時候,我們可以嘗試使用Redis來生成ID。
這主要依賴於Redis是單線程的,所以也可以用生成全局唯一的ID。可以用Redis的原子操作 INCR和INCRBY來實現。
比較適合使用Redis來生成每天從0開始的流水號。比如訂單號=日期+當日自增長號。可以每天在Redis中生成一個Key,使用INCR進行累加。
redis加lua腳本也可以實現twitter的snowflake算法。
優點
-
不依賴於數據庫,靈活方便,且性能優於數據庫。
-
數字ID天然排序,對分頁或者需要排序的結果很有幫助。
缺點
-
如果系統中沒有Redis,還需要引入新的組件,增加系統復雜度。
-
需要編碼和配置的工作量比較大。
5.參考鏈接
https://www.jianshu.com/p/da6dae36c290
https://blog.csdn.net/nawenqiang/article/details/82684001
https://segmentfault.com/a/1190000011282426
https://youzhixueyuan.com/how-to-generate-distributed-unique-id.html