• 分布式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环境)

    其他疑问

    上文代码(一直在生产环境运行)解决了朋友们的其他疑问,因此不再列出这些问题和解决方案

  • 相关阅读:
    JVM之GC调优
    JVM的栈、堆
    SpringMVC及其HandlerMapping、HandlerInterceptor、HandlerAdapter等组件的原理解析
    静态代理、动态代理和CGLIB,SpringAOP中的代理
    cmd删除文件夹
    vue-router命名视图+路由嵌套
    Vue实现长按事件
    [SparkSQL] hive.exec.max.dynamic.partitions配置不起作用
    Excel分数转为百分数
    Hadoop YARN主资源调度算法
  • 原文地址:https://www.cnblogs.com/jacobus/p/9965467.html
Copyright © 2020-2023  润新知