背景

最近有个项目:涉及到分布式计算,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等等

分布式UUID的生成的更多相关文章

  1. 分布式全局ID生成方案

    传统的单体架构的时候,我们基本是单库然后业务单表的结构.每个业务表的ID一般我们都是从1增,通过AUTO_INCREMENT=1设置自增起始值,但是在分布式服务架构模式下分库分表的设计,使得多个库或多 ...

  2. 研究分布式唯一ID生成,看完这篇就够

    很多大的互联网公司数据量很大,都采用分库分表,那么分库后就需要统一的唯一ID进行存储.这个ID可以是数字递增的,也可以是UUID类型的. 如果是递增的话,那么拆分了数据库后,可以按照id的hash,均 ...

  3. 关于分布式uuid的一点设想

    在一次公开课上,听别人讲过全局分布式uuid的设计,听过twitter的snowflake的设计.也听过,如果使用单独的计数器服务,不可能每次都保存当前计数器到文本,自己想到应该可以每隔一些数,例如1 ...

  4. SpringBoot 使用Sharding-JDBC进行分库分表及其分布式ID的生成

    为解决关系型数据库面对海量数据由于数据量过大而导致的性能问题时,将数据进行分片是行之有效的解决方案,而将集中于单一节点的数据拆分并分别存储到多个数据库或表,称为分库分表. 分库可以有效分散高并发量,分 ...

  5. Java通过UUID随机生成36位、32位唯一识别码(唯一字符串)

    import java.util.UUID; /** * 通过UUID随机生成36位.32位唯一识别码(唯一字符串) * @author [J.H] * */ public class Test { ...

  6. 分布式ID系列之为什么需要分布式ID以及生成分布式ID的业务需求

    为什么需要分布式id生成系统 在复杂分布式系统中,往往需要对大量的数据和消息进行唯一标识.如在美团点评的金融.支付.餐饮.酒店.猫眼电影等产品的系统中,数据日渐增长,对数据分库分表后需要有一个唯一ID ...

  7. 分布式唯一ID生成算法-雪花算法

    在我们的工作中,数据库某些表的字段会用到唯一的,趋势递增的订单编号,我们将介绍两种方法,一种是传统的采用随机数生成的方式,另外一种是采用当前比较流行的“分布式唯一ID生成算法-雪花算法”来实现. 一. ...

  8. 开源项目|Go 开发的一款分布式唯一 ID 生成系统

    原文连接: 开源项目|Go 开发的一款分布式唯一 ID 生成系统 今天跟大家介绍一个开源项目:id-maker,主要功能是用来在分布式环境下生成唯一 ID.上周停更了一周,也是用来开发和测试这个项目的 ...

  9. Java分布式唯一ID生成方案——比UUID效率更高的生成id工具类

    package com.xinyartech.erp.core.util; import java.lang.management.ManagementFactory; import java.net ...

随机推荐

  1. 杭电ACM2002--计算球体积

    计算球体积 Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others)Total Submi ...

  2. Asp.Net MVC中Action跳转(转载)

    首先action的跳转大致归类: 1跳转到与当前同一控制器内的action和不同控制器内的action. 2带有参数的action跳转和不带参数的action跳转. 3跳转到指定视图,不经过Contr ...

  3. 34.QT-制作串口助手(并动态检测在线串口,附带源码)

    qextserialport-1.2rc库下载链接: http://www.pudn.com/Download/item/id/2298532.html 1.添加源码到工程 将qextserialpo ...

  4. 26.C++- 泛型编程之类模板(详解)

    在上章25.C++- 泛型编程之函数模板(详解) 学习了后,本章继续来学习类模板   类模板介绍 和函数模板一样,将泛型思想应用于类. 编译器对类模板处理方式和函数模板相同,都是进行2次编译 类模板通 ...

  5. 异常: Recieved SHUTDOWN signal from Resourcemanager ,Registration of NodeManager failed, Message from ResourceManager: NodeManager from localhost doesn't satisfy minimum allocations, Sending SHUTDOWN s

    异常: Recieved SHUTDOWN signal from Resourcemanager ,Registration of NodeManager failed, Message from ...

  6. php生成xml数据

    1.php生成xml数据一般有2种方式, 一个是组装字符串,另一个就是使用php内置的系统类 2.使用php内置类生成xml数据 3.拼装字符串生成xml数据 public function stat ...

  7. Javascript 跨域知识详细介绍

    JS跨域知识总结: 在“跨域”一词经常性地出现以前,我们其实已经频繁地使用它了.如在A网站的img,src指向B网站的某一图片地址,毫无疑问,这在通常情况下都是能正常显示的(且不论防盗链技术):同样, ...

  8. vuejs2.0实现分页组件,使用$emit进行事件监听数据传递

    上一篇文章介绍了vuejs实现的简单分页,如果我有几个页面都需要有分页效果,不可能每个页面都去复制一下这段代码吧,意思是封装一下,变成通用的组件. 首先使用基础 Vue 构造器,创建一个“子类”,Vu ...

  9. vuejs-指令详解

    v-if v-if指令可以完全根据表达式的值在DOM中生成或移除一个元素.如果v-if表达式赋值为false,那么对应的元素就会从DOM中移除:否则,对应元素的一个克隆将被重新插入DOM中,代码如下: ...

  10. 26.Odoo产品分析 (三) – 人力资源板块(6) – 工资表(2)

    查看Odoo产品分析系列--目录 接上一篇Odoo产品分析 (三) – 人力资源板块(6) – 工资表(1) 4. 缴纳登记册 在"缴纳登记册"菜单中可以查看所有的登记册并新建:  ...