1. 程式人生 > >冷飯新炒:理解JDK中UUID的底層實現

冷飯新炒:理解JDK中UUID的底層實現

## 前提 `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`個字元,例如: ```shell ## 例子 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| 基於這個表格畫一個圖: ![](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202101/j-u-u-i-d-4.png) **嚴重注意,重複三次**: - 上面提到的`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`的碰撞機率呢?這是一個數學問題,可以使用比較著名的**生日悖論**解決: ![](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202101/j-u-u-i-d-1.png) 上圖來源於某搜尋引擎百科。剛好維基百科上給出了碰撞機率的計算過程,其實用的也是生日悖論的計算方法,這裡貼一下: ![](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202101/j-u-u-i-d-2.png) 上面的碰撞機率計算是基於`Leach–Salz`變體和版本`4`進行,得到的結論是: - `103`萬億個`UUID`中找到重複項的概率是十億分之一 - 要生成一個衝突率達到`50%`的`UUID`至少需要生成`2.71 * 1_000_000^3`個`UUID` 有生之年不需要擔心`UUID`衝突,出現的可能性比大型隕石撞地球還低。 ![](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202101/j-u-u-i-d-3.jpg) ### 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`形式表示,例如: ```java String uuid = UUID.randomUUID().toString(); ``` 其他`Getter`方法: ```java 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(); ``` 可以驗證一下不同靜態工廠方法的版本和變體號: ```java 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`型別的整型數值: ```java public final class UUID implements java.io.Serializable, Comparable { // 暫時省略其他程式碼 /* * 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`的其他成員屬性和建構函式: ```java public final class UUID implements java.io.Serializable, Comparable { // 暫時省略其他程式碼 // 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)`中有一些位運算技巧: ```shell 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`位 - 前面兩步的結果再進行或運算 一個模擬過程如下: ```shell (為了區分明顯,筆者每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()`,這是使用頻率最高的一個方法: ```java 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); } ``` 關於上面的位運算,這裡可以使用極端的例子進行推演: ```shell 假設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`屬性用於指定系統預設的隨機源: ![](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202101/j-u-u-i-d-5.png) 這裡要提一個小知識點,想要得到密碼學意義上的安全隨機數,可以直接使用真隨機數產生器產生的隨機數,或者使用真隨機數產生器產生的隨機數做種子。通過查詢一些資料得知**非物理真隨機數產生器**有: - `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()`: ```java 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`位元組長度的。使用方式很簡單: ```java 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`例項。 ### 其他實現 其他實現主要包括: ```java // 完全定製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`的形式,這裡詳細分析一下格式化的過程。首先從註釋上看格式是: ```shell ---- time_low = 4 * => 4個16進位制8位字元 time_mid = 2 * => 2個16進位制8位字元 time_high_and_version = 4 * => 2個16進位制8位字元 variant_and_sequence = 4 * => 2個16進位制8位字元 node = 4 * => 6個16進位制8位字元 hexOctet = (2個hexDigit) hexDigit = 0-9a-F(其實就是16進位制的字元) ``` 和前文佈局分析時候的提到的內容一致。`UUID#toString()`方法原始碼如下: ```java 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); } ``` ### 比較相關的方法 比較相關方法如下: ```java // 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](https://www.ietf.org/rfc/rfc4122.txt) - [維基百科 - Universally unique identifier](https://en.wikipedia.org/wiki/Universally_unique_identifier) - JDK11相關原始碼 留給讀者的開放性問題: - `UUID`是利用什麼特性把衝突率降到極低? - 人類有可能繁衍到`UUID`全部用完的年代嗎? (本文完 c-2-w e-a-202