背景
最近有個項目:涉及到分布式計算,tps相對較高,流程之間是異步調用,流程間相互依賴的對象(涉及記錄外鍵)需要持久化。這就衍生出了需要在JVM中快速生成分布式UUID的問題
方案
1.通過JDK標准API?UUID會重復
要生成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唯一
2.第3方組件生成UUID?性能會有損耗;單點故障
* 通過數據庫獲取UUID
通過這種消耗大量性能來獲取UUID,當然可行,但在高並發的場景下你真的會去考慮嗎?
* 基於Redis/Zookeeper做運算
網上有一些朋友會自行定義算法,借助Redis/Zookeeper來計算1個UUID,這種方案沒什么太大的問題,畢竟Redis/zookeeper的性能也不錯
不過,在復雜的多集群環境下,性能的瓶頸在於集群間的網絡時延(1次Redis集群的讀取大概10ms左右),同時這種運算多少會加重Redis和Zookeeper所在集群的負載
最重要的是,如果某個不相關的業務流程將Redis集群弄掛掉(雖然我沒有遇到過,但公司內其他的技術組還真出現過,好像是Redis集群事務問題),很容易成為單點故障,繼而影響到你的業務流程。如果是共Redis集群,即使是微服務也一樣會受到單點故障的影響
3.分布式UUID的生成 - 已在項目中運用
分布式?多台Web容器(我們可以稱之為實例)在同1個機器(mac地址相同)下?不依賴第3方工具?最好在JVM解決?
思路
-
確保每台實例具有唯一的名字(我們可以稱之為實例名)
-
確保某台實例生成的字符串不會重復: 當前系統時間 + 遞增的數值(需要避免高並發的影響,下文代碼注釋有說明)
-
利用UUID版本3的特性:使用nameUUIDFromBytes得到32位定長的唯一性字符串
因此,算法如下:
分布式UUID = nameUUIDFromBytes(實例名 + 當前系統時間毫秒數 + 遞增的Int數)
方法
-
對每台Web容器的JAVA_OPTIONS配置不一樣的實例名
以Tomcat(8.0.53)為例,在startup.bat里配置:
rem to set JAVA_OPTS set "JAVA_OPTS=%JAVA_OPTS% -Dinstance.name=JACOBUS-MBA"
這樣,上文的instance.name,就變成了JVM里的1個參數了
對於Eclipse/Idea等IDE運行環境的JAVA_OPTIONS配置,網上方法很多,不贅述
-
代碼實現
package com.mango.core.util; import java.util.UUID; import java.io.UnsupportedEncodingException; import java.util.concurrent.atomic.AtomicInteger; public class UUIDUtil { /* 從運行環境的JAVA_OPTIONS中,獲取配置:當前實例名 */ private static final String INSTANCE_NAME = System.getProperty("instance.name"); /* 計數器。AtomicInteger是java.util.concurrent下的類,JDK的算法工程師會控制好並發問題 */ private static final AtomicInteger CNT = new AtomicInteger(0); /** * 靜態方法的工具類,應該直接通過類名調用方法,因此申明private構造方法 */ private UUIDUtil() { } /** * 生成分布式UUID * * @return */ public static String getConcurrentUUID() { if (null == INSTANCE_NAME) { return "The JVM option is null, named 'instance.name'"; } String rs = null; StringBuilder sb = new StringBuilder(); sb.append(INSTANCE_NAME); sb.append(System.currentTimeMillis()); sb.append(CNT.incrementAndGet()); rs = sb.toString(); try { rs = UUID.nameUUIDFromBytes(rs.getBytes("UTF-8")).toString().replace("-", ""); } catch (UnsupportedEncodingException e) { // TODO 打印error日志,提醒getBytes異常,並打印此時的rs(即是: sb.toString();) } return rs; } }
說明
通過上文的方法可在JVM內快速生成支持分布式的UUID。如果使用了PostgreSQL,主鍵是包含短橫線的36位字符,可去掉 .replace("-", "") 部分
---End---
朋友的反饋
文章push后,有些朋友反饋了一些疑問
疑問一
問題 - 實例的JVM配置怎么管理?
有些朋友提到了實例的JVM配置問題:
- 確保多集群下的每台實例配置的實例名唯一,人為操作會出錯
- 私有雲、公有雲、混合雲的雲廠商琳琅滿目,如果我的項目跑在這些雲廠商的機器上,彈性伸縮增加的實例怎么自動配置JVM?
- 我的項目跑在docker中,這種定制化的實例名配置實在太尷尬了
可以看出,其實這些問題是同1個問題 -- 實例的JVM配置的管理
說明和方案
首先談談我的公司:公司的基礎設施建設是規范的,公司的每台Web容器都會按規范給實例配置實例名
這個問題的解決方法有多種,主要分Web容器啟動前和啟動后:
- 啟動前:運維出bash腳本,每次啟動Web應用前,會執行這個腳本,JVM里設置實例名
- 啟動后:從數據庫(或其他第3方工具)有且只取1次得到UUID,將這個UUID當成實例名
第1種演變可能會讓你覺得有些麻煩。尤其是,當你的項目跑在多種linux發行版,bash腳本會有差異,管理不同的bash可是個工作量。或則說應用跑在不同的雲廠商上,在每個雲廠商那都要配置腳本
第2種演變擺脫了各種復雜環境的影響,也不會麻煩運維的同學,同時只會讀取1次數據庫,后續的分布式UUID能在JVM中被快速生成。如果要快速在集群中動態配置唯一的實例名,建議使用第2種演變的方式實施
另外說明一下,我公司的運維同事會用統一的bash腳本管理機器(包括docker環境)
其他疑問
上文代碼(一直在生產環境運行)解決了朋友們的其他疑問,因此不再列出這些問題和解決方案