分布式UUID的生成


背景

最近有個項目:涉及到分布式計算,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數)
    

方法

  1. 對每台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配置,網上方法很多,不贅述

  2. 代碼實現

    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配置問題:

  1. 確保多集群下的每台實例配置的實例名唯一,人為操作會出錯
  2. 私有雲、公有雲、混合雲的雲廠商琳琅滿目,如果我的項目跑在這些雲廠商的機器上,彈性伸縮增加的實例怎么自動配置JVM?
  3. 我的項目跑在docker中,這種定制化的實例名配置實在太尷尬了

可以看出,其實這些問題是同1個問題 -- 實例的JVM配置的管理

說明和方案

首先談談我的公司:公司的基礎設施建設是規范的,公司的每台Web容器都會按規范給實例配置實例名

這個問題的解決方法有多種,主要分Web容器啟動前和啟動后:

  1. 啟動前:運維出bash腳本,每次啟動Web應用前,會執行這個腳本,JVM里設置實例名
  2. 啟動后:從數據庫(或其他第3方工具)有且只取1次得到UUID,將這個UUID當成實例名

第1種演變可能會讓你覺得有些麻煩。尤其是,當你的項目跑在多種linux發行版,bash腳本會有差異,管理不同的bash可是個工作量。或則說應用跑在不同的雲廠商上,在每個雲廠商那都要配置腳本

第2種演變擺脫了各種復雜環境的影響,也不會麻煩運維的同學,同時只會讀取1次數據庫,后續的分布式UUID能在JVM中被快速生成。如果要快速在集群中動態配置唯一的實例名,建議使用第2種演變的方式實施

另外說明一下,我公司的運維同事會用統一的bash腳本管理機器(包括docker環境)

其他疑問

上文代碼(一直在生產環境運行)解決了朋友們的其他疑問,因此不再列出這些問題和解決方案


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM