分散式UUID的生成
阿新 • • 發佈:2018-11-15
1.JDK標準API
要生成UUID,大多會直接使用下面這句:
UUID.randomUUID().toString().replace("-", "");
在多數情況,這樣的處理是沒問題的,畢竟是JDK標準介面。但是在某些情況下,會出現重複。搜素 uuid 重複,就會發現有人踩到了雷
先看UUID各版本的實現原理:Universally unique identifier
再看JDK的實現(只實現了UUID的1,3,4版本):java.util.UUID
會發現在分散式場景下JDK自帶的這個工具類並不好用。原因:
- 會存在多臺Web容器在同1個物理/雲主機上,mac地址相同。因此,版本1的UUID,不合適
- randomUUID實現的是UUID的版本4,產生重複的概率是可以計算出來的,海量儲存時,重複不可避免。這也是有人踩雷的原因
- nameUUIDFromBytes實現的是UUID的版本3,保證種子的唯一性才能確保生成的UUID唯一。在分散式的場景下,如果我們每次都能獲取到唯一的種子,那也就不必用這個方法生成UUID了
2.資料庫獲取UUID
通過這種消耗大量效能來獲取UUID,當然可行,但在高併發的場景下你真的會去考慮嗎?
3.分散式UUID的生成
分散式?多臺Web容器(我們可以稱之為例項)在同1個機器(mac地址相同)下?不依賴第3方工具?最好在JVM解決?
思路
確保每臺例項
確保某臺例項生成的UUID不會重複: 當前系統時間 + 遞增的數值(避免高併發的影響)
因此,演算法如下:
UUID = 例項名 + 當前系統時間毫秒數 + 遞增的int數
方法
對每臺Web容器的JAVA_OPTIONS配置不一樣的例項名
以Tomcat(8.0.53)為例,在startup.bat裡配置:
rem to set JAVA_OPTS set "JAVA_OPTS=%JAVA_OPTS% -Dinstance.name=cico.mba"
這樣,上文的instance.name,就變成了JVM裡的1個引數了
程式碼實現
package java.main; import java.util.concurrent.atomic.AtomicInteger; public class UUIDUtil { /* 從當前Web容器的JAVA_OPTIONS中,獲取JVM的配置:當前例項名 */ private static final String INSTANCE_NAME = System.getProperty("instance.name"); /* 例項名脫敏後的值 */ private static String INSTANCE_NAME_BY_NUM = null; /* 計數器。AtomicInteger是java.util.concurrent下的類,JDK的演算法工程師會避免併發問題 */ private static AtomicInteger CNT = new AtomicInteger(0); /** * 初始化INSTANCE_NAME_BY_NUM。需考慮併發 */ private synchronized static void initInstanceNameByNum() { if (null != INSTANCE_NAME_BY_NUM) { return; } char[] chars = INSTANCE_NAME.toCharArray(); StringBuilder sb = new StringBuilder(); for (char c : chars) { sb.append((int) c); } INSTANCE_NAME_BY_NUM = sb.toString(); } /** * 生成分散式的UUID * * @return */ public static String getConcurrentUUID() { if (null == INSTANCE_NAME) { return null; } if (null == INSTANCE_NAME_BY_NUM) { initInstanceNameByNum(); } StringBuilder uuid = new StringBuilder(); uuid.append(INSTANCE_NAME_BY_NUM); uuid.append(System.currentTimeMillis()); uuid.append(CNT.incrementAndGet()); return uuid.toString(); } }
說明
通過上文的方法可在JVM內快速生成支援分散式的UUID。這個UUID的長度:
- 13: System.currentTimeMillis()的長度是13位
- 11: Integer.MIN_VALUE的長度。Int值遞增,達到Int的上限後,會從負數重新計數,因此長度是11位
- 2 * 例項名的字元數。例項名一般由字母、數字、小數點、減號、下劃線組成,這些字元的ASCII碼值是2位
如果這個UUID需要持久化,持久化的欄位可定義成VARCHAR2(255),其中例項名的字元數最大可以是115 = ( 255 - 13 - 11 ) / 2