前提
UUID
是Universally Unique IDentifier
的縮寫,翻譯為通用唯一識別符號或者全域性唯一識別符號。對於UUID
的描述,下面摘錄一下規範檔案A Universally Unique IDentifier (UUID) URN Namespace
中的一些描述:
UUID(也稱為GUID)定義了統一資源名稱名稱空間。UUID的長度為128位元,可以保證在空間和時間上的唯一性。
動機:
使用UUID的主要原因之一是不需要集中式管理,其中一種格式限定了IEEE 802節點識別符號,其他格式無此限制。可以自動化按需生成UUID,應用於多重不同的場景。UUID演算法支援極高的分配速率,每臺機器每秒鐘可以生成超過1000萬個UUID,因此它們可以作為事務ID使用。UUID具有固定大小128位元,與其他替代方案相比,它具有體積小的優勢,非常適用於各種排序、雜湊和儲存在資料庫中,具有程式設計易用性的特點。
這裡只需要記住UUID
幾個核心特定:
- 全域性時空唯一性
- 固定長度
128
位元,也就是16
位元組(1 byte = 8 bit
) - 分配速率極高,單機每秒可以生成超過
1000
萬個UUID
(實際上更高)
下面就JDK
中的UUID
實現詳細分析一下UUID
生成演算法。編寫本文的時候選用的JDK
為JDK11
。
再聊UUID
前面為了編寫簡單的摘要,所以只粗略摘錄了規範檔案裡面的一些章節,這裡再詳細聊聊UUID
的一些定義、碰撞概率等等。
UUID定義
UUID
是一種軟體構建的標準,也是開放軟體基金會組織在分散式計算環境領域的一部分。提出此標準的目的是:讓分散式系統中的所有元素或者元件都有唯一的可辨別的資訊,因為極低衝突頻率和高效演算法的基礎,它不需要集中式控制和管理唯一可辨別資訊的生成,由此,每個使用者都可以自由地建立與其他人不衝突的UUID
。
UUID
本質是一個128
位元的數字,這是一個位長巨大的數值,理論上來說,UUID
的總數量為2^128
個。這個數字大概可以這樣估算:如果每納秒產生1兆個不相同的UUID
,需要花費超過100
億年才會用完所有的UUID
。
UUID的變體與版本
UUID
標準和演算法定義的時候,為了考慮歷史相容性和未來的擴充套件,提供了多種變體和版本。接下來的變體和版本描述來源於維基百科中的Versions
章節和RFC 4122
中的Variant
章節。
目前已知的變體如下:
- 變體
0xx
:Reserved, NCS backward compatibility
,為向後相容做預留的變體 - 變體
10x
:The IETF aka Leach-Salz variant (used by this class)
,稱為Leach–Salz UUID
或者IETF UUID
,JDK
中UUID
目前正在使用的變體 - 變體
110
:Reserved, Microsoft Corporation backward compatibility
,微軟早期GUID
預留變體 - 變體
111
:Reserved for future definition
,將來擴充套件預留,目前還沒被使用的變體
目前已知的版本如下:
- 空
UUID
(特殊版本0
),用00000000-0000-0000-0000-000000000000
表示,也就是所有的位元都是0
date-time and MAC address
(版本1
):基於時間和MAC
地址的版本,通過計算當前時間戳、隨機數和機器MAC
地址得到。由於有MAC
地址,這個可以保證其在全球的唯一性。但是使用了MAC
地址,就會有MAC
地址暴露問題。若是區域網,可以用IP
地址代替date-time and MAC address, DCE security version
(版本2
):分散式計算環境安全的UUID
,演算法和版本1
基本一致,但會把時間戳的前4
位置換為POSIX
的UID
或GID
namespace name-based MD5
(版本3
):通過計算名字和名稱空間的MD5
雜湊值得到。這個版本的UUID
保證了:相同名稱空間中不同名字生成的UUID
的唯一性;不同名稱空間中的UUID
的唯一性;相同名稱空間中相同名字的UUID
重複生成是相同的random
(版本4
):根據隨機數,或者偽隨機數生成UUID
。這種UUID
產生重複的概率是可以計算出來的,還有一個特點就是預留了6
位元存放變體和版本屬性,所以隨機生成的位一共有122
個,總量為2^122
,比其他變體的總量要偏少namespace name-based SHA-1
(版本5
):和版本3
類似,雜湊演算法換成了SHA-1
其中,JDK
中應用的變體是Leach-Salz
,提供了namespace name-based MD5
(版本3
)和random
(版本4
)兩個版本的UUID
生成實現。
UUID的格式
在規範檔案描述中,UUID
是由16
個8
位元數字,或者說32
個16
進製表示形式下的字元組成,一般表示形式為8-4-4-4-12
,加上連線字元-
一共有36
個字元,例如:
## 例子
123e4567-e89b-12d3-a456-426614174000
## 通用格式
xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx
其中4
位元長度的M
和1
到3
位元長度的N
分別代表版本號和變體標識。UUID
的具體佈局如下:
屬性 | 屬性名 | 長度(bytes ) |
長度(16 進位制字元) |
內容 |
---|---|---|---|---|
time_low |
時間戳低位 | 4 | 8 | 代表時間戳的低32 位元的整數表示 |
time_mid |
時間戳中位 | 2 | 4 | 代表時間戳的中間16 位元的整數表示 |
time_hi_and_version |
時間戳高位和版本號 | 2 | 4 | 高位4 位元是版本號表示,剩餘是時間戳的高12 位元的整數表示 |
clock_seq_hi_and_res clock_seq_low |
時鐘序列與變體編號 | 2 | 4 | 最高位1 到3 位元表示變體編號,剩下的13 到15 位元表示時鐘序列 |
node |
節點ID | 6 | 12 | 48 位元表示的節點ID |
基於這個表格畫一個圖:
嚴重注意,重複三次:
- 上面提到的
UUID
的具體佈局只適用於date-time and MAC address
(版本1
)和date-time and MAC address, DCE security version
(版本2
),其他版本雖然採用了基本一樣的欄位分佈,但是無法獲取時間戳、時鐘序列或者節點ID
等資訊 - 上面提到的
UUID
的具體佈局只適用於date-time and MAC address
(版本1
)和date-time and MAC address, DCE security version
(版本2
),其他版本雖然採用了基本一樣的欄位分佈,但是無法獲取時間戳、時鐘序列或者節點ID
等資訊 - 上面提到的
UUID
的具體佈局只適用於date-time and MAC address
(版本1
)和date-time and MAC address, DCE security version
(版本2
),其他版本雖然採用了基本一樣的欄位分佈,但是無法獲取時間戳、時鐘序列或者節點ID
等資訊
JDK中只提供了版本3和版本4的實現,但是java.util.UUID的佈局採用了上面表格的欄位
UUID的碰撞機率計算
UUID
的總量雖然巨大,但是如果不停地使用,假設每納秒生成超過1
兆個UUID
並且人類有幸能夠繁衍到100
億年以後,總會有可能產生重複的UUID
。那麼,怎麼計算UUID
的碰撞機率呢?這是一個數學問題,可以使用比較著名的生日悖論解決:
上圖來源於某搜尋引擎百科。剛好維基百科上給出了碰撞機率的計算過程,其實用的也是生日悖論的計算方法,這裡貼一下:
上面的碰撞機率計算是基於Leach–Salz
變體和版本4
進行,得到的結論是:
103
萬億個UUID
中找到重複項的概率是十億分之一- 要生成一個衝突率達到
50%
的UUID
至少需要生成2.71 * 1_000_000^3
個UUID
有生之年不需要擔心UUID
衝突,出現的可能性比大型隕石撞地球還低。
UUID的使用場景
基本所有需要使用全域性唯一識別符號的場景都可以使用UUID
,除非對長度有明確的限制,常用的場景包括:
- 日誌框架對映診斷上下文中的
TRACE_ID
APM
工具或者說OpenTracing
規範中的SPAN_ID
- 特殊場景下資料庫主鍵或者虛擬外來鍵
- 交易
ID
(訂單ID
) - 等等......
JDK中UUID詳細介紹和使用
這裡先介紹使用方式。前面提到JDK
中應用的變體是Leach-Salz
(變體2
),提供了namespace name-based MD5
(版本3
)和random
(版本4
)兩個版本的UUID
生成實現,實際上java.util.UUID
提供了四種生成UUID
例項的方式:
- 最常見的就是呼叫靜態方法
UUID#randomUUID()
,這就是版本4
的靜態工廠方法 - 其次是呼叫靜態方法
UUID#nameUUIDFromBytes(byte[] name)
,這就是版本3
的靜態工廠方法 - 另外有呼叫靜態方法
UUID#fromString(String name)
,這是解析8-4-4-4-12
格式字串生成UUID
例項的靜態工廠方法 - 還有低層次的建構函式
UUID(long mostSigBits, long leastSigBits)
,這個對於使用者來說並不常見
最常用的方法有例項方法toString()
,把UUID
轉化為16
進位制字串拼接而成的8-4-4-4-12
形式表示,例如:
String uuid = UUID.randomUUID().toString();
其他Getter
方法:
UUID uuid = UUID.randomUUID();
// 返回版本號
int version = uuid.version();
// 返回變體號
int variant = uuid.variant();
// 返回時間戳 - 這個方法會報錯,只有Time-based UUID也就是版本1或者2的UUID實現才能返回時間戳
long timestamp = uuid.timestamp();
// 返回時鐘序列 - 這個方法會報錯,只有Time-based UUID也就是版本1或者2的UUID實現才能返回時鐘序列
long clockSequence = uuid.clockSequence();
// 返回節點ID - 這個方法會報錯,只有Time-based UUID也就是版本1或者2的UUID實現才能返回節點ID
long nodeId = uuid.node();
可以驗證一下不同靜態工廠方法的版本和變體號:
UUID uuid = UUID.randomUUID();
int version = uuid.version();
int variant = uuid.variant();
System.out.println(String.format("version:%d,variant:%d", version, variant));
uuid = UUID.nameUUIDFromBytes(new byte[0]);
version = uuid.version();
variant = uuid.variant();
System.out.println(String.format("version:%d,variant:%d", version, variant));
// 輸出結果
version:4,variant:2
version:3,variant:2
探究JDK中UUID原始碼實現
java.util.UUID
被final
修飾,實現了Serializable
和Comparable
介面,從一般理解上看,有下面的特定:
- 不可變,一般來說工具類都是這樣定義的
- 可序列化和反序列化
- 不同的物件之間可以進行比較,比較方法後面會分析
下面會從不同的方面分析一下java.util.UUID
的原始碼實現:
- 屬性和建構函式
- 隨機數版本實現
- namespace name-based MD5版本實現
- 其他實現
- 格式化輸出
- 比較相關的方法
屬性和建構函式
前面反覆提到JDK
中只提供了版本3
和版本4
的實現,但是java.util.UUID
的佈局採用了UUID
規範中的欄位定義,長度一共128
位元,剛好可以存放在兩個long
型別的整數中,所以看到了UUID
類中存在兩個long
型別的整型數值:
public final class UUID implements java.io.Serializable, Comparable<UUID> {
// 暫時省略其他程式碼
/*
* The most significant 64 bits of this UUID.
* UUID中有效的高64位元
*
* @serial
*/
private final long mostSigBits;
/*
* The least significant 64 bits of this UUID.
* UUID中有效的低64位元
*
* @serial
*/
private final long leastSigBits;
// 暫時省略其他程式碼
}
從UUID
類註釋中可以看到具體的欄位佈局如下:
高64
位元mostSigBits
的佈局
欄位 | bit 長度 |
16 進位制字元長度 |
---|---|---|
time_low |
32 | 8 |
time_mid |
16 | 4 |
version |
4 | 1 |
time_hi |
12 | 3 |
低64
位元leastSigBits
的佈局
欄位 | bit 長度 |
16 進位制字元長度 |
---|---|---|
variant |
2 | 小於1 |
clock_seq |
14 | variant 和clock_seq 加起來等於4 |
node |
48 | 12 |
接著看UUID
的其他成員屬性和建構函式:
public final class UUID implements java.io.Serializable, Comparable<UUID> {
// 暫時省略其他程式碼
// Java語言訪問類,裡面存放了很多底層相關的訪問或者轉換方法,在UUID中主要是toString()例項方法用來格式化成8-4-4-4-12的形式,委託到Long.fastUUID()方法
private static final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
// 靜態內部類確保SecureRandom初始化,用於版本4的隨機數UUID版本生成安全隨機數
private static class Holder {
static final SecureRandom numberGenerator = new SecureRandom();
}
// 通過長度為16的位元組陣列,計算mostSigBits和leastSigBits的值初始化UUID例項
private UUID(byte[] data) {
long msb = 0;
long lsb = 0;
assert data.length == 16 : "data must be 16 bytes in length";
for (int i=0; i<8; i++)
msb = (msb << 8) | (data[i] & 0xff);
for (int i=8; i<16; i++)
lsb = (lsb << 8) | (data[i] & 0xff);
this.mostSigBits = msb;
this.leastSigBits = lsb;
}
// 直接指定mostSigBits和leastSigBits構造UUID例項
public UUID(long mostSigBits, long leastSigBits) {
this.mostSigBits = mostSigBits;
this.leastSigBits = leastSigBits;
}
// 暫時省略其他程式碼
}
私有構造private UUID(byte[] data)
中有一些位運算技巧:
long msb = 0;
long lsb = 0;
assert data.length == 16 : "data must be 16 bytes in length";
for (int i=0; i<8; i++)
msb = (msb << 8) | (data[i] & 0xff);
for (int i=8; i<16; i++)
lsb = (lsb << 8) | (data[i] & 0xff);
this.mostSigBits = msb;
this.leastSigBits = lsb;
輸入的位元組陣列長度為16
,mostSigBits
由位元組陣列的前8
個位元組轉換而來,而leastSigBits
由位元組陣列的後8
個位元組轉換而來。中間變數msb
或者lsb
在提取位元組位進行計算的時候:
- 先進行左移
8
位確保需要計算的位為0
,已經計算好的位移動到左邊 - 然後右邊需要提取的位元組
data[i]
的8
位會先和0xff
(補碼1111 1111
)進行或運算,確保不足8
位的高位被補充為0
,超過8
位的高位會被截斷為低8
位,也就是data[i] & 0xff
確保得到的補碼為8
位 - 前面兩步的結果再進行或運算
一個模擬過程如下:
(為了區分明顯,筆者每4位加了一個下劃線)
(為了簡答,只看位元組陣列的前4個位元組,同時只看long型別的前4個位元組)
0xff === 1111_1111
long msb = 0 => 0000_0000 0000_0000 0000_0000 0000_0000
byte[] data
0000_0001 0000_0010 0000_0100 0000_1000
i = 0(第一輪)
msb << 8 = 0000_0000 0000_0000 0000_0000 0000_0000
data[i] & 0xff = 0000_0001 & 1111_1111 = 0000_0001
(msb << 8) | (data[i] & 0xff) = 0000_0000 0000_0000 0000_0000 0000_0001
(第一輪 msb = 0000_0000 0000_0000 0000_0000 0000_0001)
i = 1(第二輪)
msb << 8 = 0000_0000 0000_0000 0000_0001 0000_0000
data[i] & 0xff = 0000_0010 & 1111_1111 = 0000_0010
(msb << 8) | (data[i] & 0xff) = 0000_0000 0000_0000 0000_0001 0000_0010
(第二輪 msb = 0000_0000 0000_0000 0000_0001 0000_0010)
i = 2(第三輪)
msb << 8 = 0000_0000 0000_0001 0000_0010 0000_0000
data[i] & 0xff = 0000_0100 & 1111_1111 = 0000_0100
(msb << 8) | (data[i] & 0xff) = 0000_0000 0000_0001 0000_0010 0000_0100
(第三輪 msb = 0000_0000 0000_0001 0000_0010 0000_0100)
i = 3(第四輪)
msb << 8 = 0000_0001 0000_0010 0000_0100 0000000
data[i] & 0xff = 0000_1000 & 1111_1111 = 0000_1000
(msb << 8) | (data[i] & 0xff) = 0000_0001 0000_0010 0000_0100 0000_1000
(第四輪 msb = 0000_0001 0000_0010 0000_0100 0000_1000)
以此類推,這個私有建構函式執行完畢後,長度為16
的位元組陣列的所有位就會轉移到mostSigBits
和leastSigBits
中。
隨機數版本實現
建構函式分析完,接著分析重磅的靜態工廠方法UUID#randomUUID()
,這是使用頻率最高的一個方法:
public static UUID randomUUID() {
// 靜態內部類Holder持有的SecureRandom例項,確保提前初始化
SecureRandom ng = Holder.numberGenerator;
// 生成一個16位元組的安全隨機數,放在長度為16的位元組陣列中
byte[] randomBytes = new byte[16];
ng.nextBytes(randomBytes);
// 清空版本號所在的位,重新設定為4
randomBytes[6] &= 0x0f; /* clear version */
randomBytes[6] |= 0x40; /* set to version 4 */
// 清空變體號所在的位,重新設定為2
randomBytes[8] &= 0x3f; /* clear variant */
randomBytes[8] |= 0x80; /* set to IETF variant */
return new UUID(randomBytes);
}
關於上面的位運算,這裡可以使用極端的例子進行推演:
假設randomBytes[6] = 1111_1111
// 清空version位
randomBytes[6] &= 0x0f => 1111_1111 & 0000_1111 = 0000_1111
得到randomBytes[6] = 0000_1111 (這裡可見高4位元被清空為0)
// 設定version位為整數4 => 十六進位制0x40 => 二級制補碼0100_0000
randomBytes[6] |= 0x40 => 0000_1111 | 0100_0000 = 0100_1111
得到randomBytes[6] = 0100_1111
結果:version位 => 0100(4 bit)=> 對應十進位制數4
同理
假設randomBytes[8] = 1111_1111
// 清空variant位
randomBytes[8] &= 0x3f => 1111_1111 & 0011_1111 = 0011_1111
// 設定variant位為整數128 => 十六進位制0x80 => 二級制補碼1000_0000 (這裡取左邊高位2位)
randomBytes[8] |= 0x80 => 0011_1111 | 1000_0000 = 1011_1111
結果:variant位 => 10(2 bit)=> 對應十進位制數2
關於UUID
裡面的Getter
方法例如version()
、variant()
其實就是找到對應的位,並且轉換為十進位制整數返回,如果熟練使用位運算,應該不難理解,後面不會分析這類的Getter
方法。
隨機數版本實現強依賴於SecureRandom
生成的隨機數(位元組陣列)。SecureRandom
的引擎提供者可以從sun.security.provider.SunEntries
中檢視,對於不同系統版本的JDK
實現會選用不同的引擎,常見的如NativePRNG
。JDK11
配置檔案$JAVA_HOME/conf/security/java.security
中的securerandom.source
屬性用於指定系統預設的隨機源:
這裡要提一個小知識點,想要得到密碼學意義上的安全隨機數,可以直接使用真隨機數產生器產生的隨機數,或者使用真隨機數產生器產生的隨機數做種子。通過查詢一些資料得知非物理真隨機數產生器有:
Linux
作業系統的/dev/random
裝置介面Windows
作業系統的CryptGenRandom
介面
如果不修改java.security
配置檔案,預設隨機數提供引擎會根據不同的作業系統選用不同的實現,這裡不進行深究。在Linux
環境下,SecureRandom
例項化後,不通過setSeed()
方法設定隨機數作為種子,預設就是使用/dev/random
提供的安全隨機數介面獲取種子,產生的隨機數是密碼學意義上的安全隨機數。一句話概括,UUID
中的私有靜態內部類Holder
中的SecureRandom
例項可以產生安全隨機數,這個是JDK
實現UUID
版本4
的一個重要前提。這裡總結一下隨機數版本UUID
的實現步驟:
- 通過
SecureRandom
依賴提供的安全隨機數介面獲取種子,生成一個16
位元組的隨機數(位元組陣列) - 對於生成的隨機數,清空和重新設定
version
和variant
對應的位 - 把重置完
version
和variant
的隨機數的所有位轉移到mostSigBits
和leastSigBits
中
namespace name-based MD5版本實現
接著分析版本3
也就是namespace name-based MD5
版本的實現,對應於靜態工廠方法UUID#nameUUIDFromBytes()
:
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);
}
它的後續基本處理和隨機數版本基本一致(清空版本位的時候,重新設定為3
),唯一明顯不同的地方就是生成原始隨機數的時候,採用的方式是:基於輸入的name
位元組陣列,通過MD5
摘要演算法生成一個MD5
摘要位元組陣列作為原始安全隨機數,返回的這個隨機數剛好也是16
位元組長度的。使用方式很簡單:
UUID uuid = UUID.nameUUIDFromBytes("throwable".getBytes());
namespace name-based MD5
版本UUID
的實現步驟如下:
- 通過輸入的命名位元組陣列基於
MD5
演算法生成一個16
位元組長度的隨機數 - 對於生成的隨機數,清空和重新設定
version
和variant
對應的位 - 把重置完
version
和variant
的隨機數的所有位轉移到mostSigBits
和leastSigBits
中
namespace name-based MD5
版本的UUID
強依賴於MD5
演算法,有個明顯的特徵是如果輸入的byte[] name
一致的情況下,會產生完全相同的UUID
例項。
其他實現
其他實現主要包括:
// 完全定製mostSigBits和leastSigBits,可以參考UUID標準欄位佈局進行設定,也可以按照自行制定的標準
public UUID(long mostSigBits, long leastSigBits) {
this.mostSigBits = mostSigBits;
this.leastSigBits = leastSigBits;
}
// 基於字串格式8-4-4-4-12的UUID輸入,重新解析出mostSigBits和leastSigBits,這個靜態工廠方法也不常用,裡面的位運算也不進行詳細探究
public static UUID fromString(String name) {
int len = name.length();
if (len > 36) {
throw new IllegalArgumentException("UUID string too large");
}
int dash1 = name.indexOf('-', 0);
int dash2 = name.indexOf('-', dash1 + 1);
int dash3 = name.indexOf('-', dash2 + 1);
int dash4 = name.indexOf('-', dash3 + 1);
int dash5 = name.indexOf('-', dash4 + 1);
if (dash4 < 0 || dash5 >= 0) {
throw new IllegalArgumentException("Invalid UUID string: " + name);
}
long mostSigBits = Long.parseLong(name, 0, dash1, 16) & 0xffffffffL;
mostSigBits <<= 16;
mostSigBits |= Long.parseLong(name, dash1 + 1, dash2, 16) & 0xffffL;
mostSigBits <<= 16;
mostSigBits |= Long.parseLong(name, dash2 + 1, dash3, 16) & 0xffffL;
long leastSigBits = Long.parseLong(name, dash3 + 1, dash4, 16) & 0xffffL;
leastSigBits <<= 48;
leastSigBits |= Long.parseLong(name, dash4 + 1, len, 16) & 0xffffffffffffL;
return new UUID(mostSigBits, leastSigBits);
}
格式化輸出
格式化輸出體現在UUID#toString()
方法,這個方法會把mostSigBits
和leastSigBits
格式化為8-4-4-4-12
的形式,這裡詳細分析一下格式化的過程。首先從註釋上看格式是:
<time_low>-<time_mid>-<time_high_and_version>-<variant_and_sequence>-<node>
time_low = 4 * <hexOctet> => 4個16進位制8位字元
time_mid = 2 * <hexOctet> => 2個16進位制8位字元
time_high_and_version = 4 * <hexOctet> => 2個16進位制8位字元
variant_and_sequence = 4 * <hexOctet> => 2個16進位制8位字元
node = 4 * <hexOctet> => 6個16進位制8位字元
hexOctet = <hexDigit><hexDigit>(2個hexDigit)
hexDigit = 0-9a-F(其實就是16進位制的字元)
和前文佈局分析時候的提到的內容一致。UUID#toString()
方法原始碼如下:
private static final JavaLangAccess jla = SharedSecrets.getJavaLangAccess();
public String toString() {
return jla.fastUUID(leastSigBits, mostSigBits);
}
↓↓↓↓↓↓↓↓↓↓↓↓
// java.lang.System
private static void setJavaLangAccess() {
SharedSecrets.setJavaLangAccess(new JavaLangAccess() {
public String fastUUID(long lsb, long msb) {
return Long.fastUUID(lsb, msb);
}
}
↓↓↓↓↓↓↓↓↓↓↓↓
// java.lang.Long
static String fastUUID(long lsb, long msb) {
// COMPACT_STRINGS在String類中預設為true,所以會命中if分支
if (COMPACT_STRINGS) {
// 初始化36長度的位元組陣列
byte[] buf = new byte[36];
// lsb的低48位轉換為16進位制格式寫入到buf中 - node => 位置[24,35]
formatUnsignedLong0(lsb, 4, buf, 24, 12);
// lsb的高16位轉換為16進位制格式寫入到buf中 - variant_and_sequence => 位置[19,22]
formatUnsignedLong0(lsb >>> 48, 4, buf, 19, 4);
// msb的低16位轉換為16進位制格式寫入到buf中 - time_high_and_version => 位置[14,17]
formatUnsignedLong0(msb, 4, buf, 14, 4);
// msb的中16位轉換為16進位制格式寫入到buf中 - time_mid => 位置[9,12]
formatUnsignedLong0(msb >>> 16, 4, buf, 9, 4);
// msb的高32位轉換為16進位制格式寫入到buf中 - time_low => 位置[0,7]
formatUnsignedLong0(msb >>> 32, 4, buf, 0, 8);
// 空餘的位元組槽位插入'-',剛好佔用了4個位元組
buf[23] = '-';
buf[18] = '-';
buf[13] = '-';
buf[8] = '-';
// 基於處理好的位元組陣列,例項化String,並且編碼指定為LATIN1
return new String(buf, LATIN1);
} else {
byte[] buf = new byte[72];
formatUnsignedLong0UTF16(lsb, 4, buf, 24, 12);
formatUnsignedLong0UTF16(lsb >>> 48, 4, buf, 19, 4);
formatUnsignedLong0UTF16(msb, 4, buf, 14, 4);
formatUnsignedLong0UTF16(msb >>> 16, 4, buf, 9, 4);
formatUnsignedLong0UTF16(msb >>> 32, 4, buf, 0, 8);
StringUTF16.putChar(buf, 23, '-');
StringUTF16.putChar(buf, 18, '-');
StringUTF16.putChar(buf, 13, '-');
StringUTF16.putChar(buf, 8, '-');
return new String(buf, UTF16);
}
}
/**
* 格式化無符號的長整型,填充到位元組緩衝區buf中,如果長度len超過了輸入值的ASCII格式表示,則會使用0進行填充
* 這個方法就是把輸入長整型值val,對應一段長度的位,填充到位元組陣列buf中,len控制寫入字元的長度,offset控制寫入buf的起始位置
* 而shift引數決定基礎格式,4是16進位制,1是2進位制,3是8位
*/
static void formatUnsignedLong0(long val, int shift, byte[] buf, int offset, int len) {
int charPos = offset + len;
int radix = 1 << shift;
int mask = radix - 1;
do {
buf[--charPos] = (byte)Integer.digits[((int) val) & mask];
val >>>= shift;
} while (charPos > offset);
}
比較相關的方法
比較相關方法如下:
// hashCode方法基於mostSigBits和leastSigBits做異或得出一箇中間變數hilo,再以32為因子進行計算
public int hashCode() {
long hilo = mostSigBits ^ leastSigBits;
return ((int)(hilo >> 32)) ^ (int) hilo;
}
// equals為例項對比方法,直接對比兩個UUID的mostSigBits和leastSigBits值,完全相等的時候返回true
public boolean equals(Object obj) {
if ((null == obj) || (obj.getClass() != UUID.class))
return false;
UUID id = (UUID)obj;
return (mostSigBits == id.mostSigBits &&
leastSigBits == id.leastSigBits);
}
// 比較規則是mostSigBits高位大者為大,高位相等的情況下,leastSigBits大者為大
public int compareTo(UUID val) {
// The ordering is intentionally set up so that the UUIDs
// can simply be numerically compared as two numbers
return (this.mostSigBits < val.mostSigBits ? -1 :
(this.mostSigBits > val.mostSigBits ? 1 :
(this.leastSigBits < val.leastSigBits ? -1 :
(this.leastSigBits > val.leastSigBits ? 1 :
0))));
}
所有比較方法僅僅和mostSigBits
和leastSigBits
有關,畢竟這兩個長整型就儲存了UUID
例項的所有資訊。
小結
縱觀UUID
的原始碼實現,會發現了除了一些精巧的位運算,它的實現是依賴於一些已經完備的功能,包括MD5
摘要演算法和SecureRandom
依賴系統隨機源產生安全隨機數。UUID
之所以能夠成為一種標準,是因為它凝聚了計算機領域前輩鑽研多年的成果,所以現在使用者才能像寫Hello World
那樣簡單呼叫UUID.randomUUID()
。
參考資料:
- RFC 4122
- 維基百科 - Universally unique identifier
- JDK11相關原始碼
留給讀者的開放性問題:
UUID
是利用什麼特性把衝突率降到極低?- 人類有可能繁衍到
UUID
全部用完的年代嗎?
(本文完 c-2-w e-a-20210129)