背景

最近有个项目:涉及到分布式计算,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唯一。在分布式的场景下,如果我们每次都能获取到唯一的种子,那也就不必用这个方法生成UUID了

2.第3方组件生成UUID?性能会有损耗;单点故障

* 通过数据库获取UUID

通过这种消耗大量性能来获取UUID,当然可行,但在高并发的场景下你真的会去考虑吗?

* 基于Redis/Zookeeper做运算

网上有一些朋友会自行定义算法,借助Redis/Zookeeper来计算1个UUID,这种方案没什么太大的问题,毕竟Redis/zookeeper的性能也不错

不过,在复杂的多集群环境下,性能的瓶颈在于集群间的网络时延(1次Redis集群的读取大概50ms),同时这种运算多少会加重Redis和Zookeeper所在集群的负载

最重要的是,如果某个不相关的业务流程将Redis集群弄挂掉(虽然我没有遇到过,但公司内其他的技术组还真出现过,好像是Redis集群事务问题),很容易成为单点故障,继而影响到你的业务流程。如果是共Redis集群,即使是微服务也一样会受到单点故障的影响

3.分布式UUID的生成 - 已在项目中运用

分布式?多台Web容器(我们可以称之为实例)在同1个机器(mac地址相同)下?不依赖第3方工具?最好在JVM解决?

思路

  • 确保每台实例具有唯一的名字(我们可以称之为实例名)

  • 确保某台实例生成的UUID不会重复: 当前系统时间 + 递增的数值(避免高并发的影响)

因此,算法如下:

UUID = 实例名 + 当前系统时间毫秒数 + 递增的Int数

方法

  1. 对每台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个参数了

  2. 代码实现

    package com.cucurbit.core.util;
    
    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.toUpperCase().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 "The JVM option is null, named 'instance.name'";
            }
            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的长度,由下面3部分组成:

  • 13: System.currentTimeMillis()的长度是13位
  • 11: Integer.MIN_VALUE的长度。Int值从0开始递增,达到Int的上限后,会从负数开始重新计数,因此长度最大是11位
  • 2 * 实例名的字符数: 实例名(被转成了全大写)一般由字母、数字、小数点、减号、下划线组成,这些字符的ASCII码值是2位

如果这个UUID需要持久化,持久化的字段可定义成VARCHAR2(255),其中实例名的字符长度最大可以是115 = ( 255 - 13 - 11 ) / 2

---End---


朋友的反馈

文章publish后,有些朋友反馈了一些疑问

疑问一

问题 - 实例的JVM配置怎么管理?

有些朋友提到了实例的JVM配置问题:

  1. 确保多集群下的每台实例配置的实例名唯一,人为操作会出错
  2. 私有云、公有云、混合云的云厂商琳琅满目,如果我的项目跑在这些云厂商的机器上,弹性伸缩增加的实例怎么自动配置JVM?
  3. 我的项目跑在docker中,这种定制化的实例名配置实在太尴尬了

可以看出,其实这些问题是同1个问题 -- 实例的JVM配置的管理

说明和方案

首先谈谈我的公司:公司的基础设施建设是规范的,公司的每台Web容器都会按规范给实例配置实例名

这个问题的解决方法有多种,主要分Web容器启动前和启动后:

  1. 启动前:运维出bash脚本,每次启动Web应用前,会执行这个脚本,动态设置JVM。实例名 = 机器MAC地址 + 自增Int值
  2. 启动后:从数据库(或其他第3方工具)有且只取1次得到UUID,将这个UUID当成实例名

第1种演变可能会让你觉得有些麻烦。尤其是,当你的项目跑在多种linux发行版,bash脚本会有差异,管理不同的bash可是个工作量。或则说应用跑在不同的云厂商上,在每个云厂商那都要配置脚本

第2种演变摆脱了各种复杂环境的影响,同时只会读取1次数据库,后续的分布式UUID也能在JVM中被快速生成。如果要在集群中动态配置唯一的实例名,建议使用第2种演变的方式实施

疑问二

问题 - 小写字母的ASCII码

有朋友指出小写字母的ASCII码存在3位的情况,上文提到的实例名的最大长度会小于115

说明和方案

这位朋友的说法是正确的。说明一下:作为技术人员的公德,不会去上传公司的代码,博客里的代码是在去掉核心业务逻辑的基础上,重写的工具型代码。在公司的代码库中,实例名会先被转成全大写后再做后面的运算,重写时确实没有加上这段

调整了一下代码和部分说明

疑问三

问题 - 能否压缩一下UUID的长度?

这个问题其实也被我的项目中的测试同事询问过。虽然UUID的总长度不会超过255,但是太长了也不太好吧?

说明和方案

先说我的项目:很抱歉,虽然测试说了那么一句,但没有去优化,哈哈~

当时没有整改,主要是因为项目前期为了性能而不择手段,不想有多余的性能损耗。不过确实可以优化,毕竟为这个项目而开发的分布式计算调度引擎的性能确实挺好的,1个255位的字符压缩,耗不了多少性能

要压缩?方法那就太多了:md5,或则上文提到的java.util.UUID.nameUUIDFromBytes等等

最新文章

  1. 百度地图api调用
  2. links and softwares
  3. jquery之右下角消息提示框
  4. Nightwatch.js – 轻松实现浏览器的自动测试
  5. Java异常与异常处理简单使用
  6. ARC指南2 - ARC的开启和禁止
  7. jquery删除原事件
  8. 【Apache运维基础(2)】主配置文件说明
  9. Codeforces Gym 100513M M. Variable Shadowing 暴力
  10. Angular2 从0到1 (一)
  11. 关于LOAD DATA INFILE 命令的使用问题解决
  12. boost库在工作(40)串行通讯
  13. 2.13.1. 对结果排序(Core Data 应用程序实践指南)
  14. 在WIN SERVER 2016上安装DOCKER(带过坑)
  15. .haccess 配置生效
  16. web开发中如何使用引用字体
  17. MVC过滤器处理Session过期
  18. 1. qt 入门-整体框架
  19. 20155208徐子涵 2016-2017-2 《Java程序设计》第7周学习总结
  20. WayOS计费对接(零点计费系统)详细教程

热门文章

  1. 走过路过不要错过 包你一文看懂支撑向量机SVM
  2. WPF 获取DataGrid 控件选中的单元格信息
  3. Asp.net连接数据库的配置方法
  4. 同一个dll 不同路径下注册 一个失败 一个成功
  5. php 设计模式之单例模式
  6. Android文件的流操作工具类
  7. CA 工作流程
  8. layui 弹出框改变按钮颜色样式 自定义皮肤
  9. 冒泡排序算法的C++实现
  10. C程序