关于最近写分布式相关的程序遇到了随机UUID在多线程环境下可能会生成重复的UUID的坑

最近正在写一个IDS相关的监控探针,在万级甚至百万级的数据量下,通过redis做中间件进行负载,先将接收到的数据储存于redis,然后再进行数据分析处理

这种涉及到分布式计算的程序,使用单线程肯定是不可以的,很容易造成堵塞。最开始采用的是多线程+redis,获取13位时间戳做存储key。

实际测试的时间,多线程高并发高数据量的情况下,1万条数据确实能够在短短不到5s的时间内存储完毕,但是却发现redis中存储的数据仅仅只有少数几条,这才意识到,多线程下很容易会出现数据覆盖的情况。

为了防止出现这种现象,我准备使用随机UUID与分布式锁进行数据的存储,我这里使用的分布式锁中的红锁(RedLock),红锁我就不多介绍了。

估计很多人都应该知道,大多数情况下都会这样获取随机UUID:

     /**
     * 获取随机UUID
     *
     * @return 随机UUID
     */
    public static String randomUUID()
    {
        return UUID.randomUUID().toString();
    }

    /**
     * 简化的UUID,去掉了横线
     *
     * @return 简化的UUID,去掉了横线
     */
    public static String simpleUUID()
    {
        return UUID.randomUUID().toString().replace("-", "");
    }

在多数情况下,这样的处理是没问题的,毕竟是JDK标准接口。

然而,我调整后代码重新测试,再次发现一个大坑,jdk自带的工具java.util.UUID在高并发,多线程,海量数据情况下会出现重复UUID的生成。。。

会发现在分布式场景下JDK自带的这个工具类并不好用。原因:

会存在多台Web容器在同1个物理/云主机上,mac地址相同。因此,最基本的UUID,不合适

randomUUID实现的是UUID的版本4,产生重复的概率是可以计算出来的,海量存储时,重复不可避免。这也是有人踩雷的原因

nameUUIDFromBytes实现的是UUID的版本3,保证种子的唯一性才能确保生成的UUID唯一。在分布式的场景下,如果我们每次都能获取到唯一的种子,那也就不必用这个方法生成UUID了。

我查阅了很多博主文章和文档,发现了很多方案并不适合我的项目,就比如:

* 通过数据库获取UUID

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

* 基于Redis/Zookeeper做运算

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

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

最重要的是,如果某个不相关的业务流程将Redis集群弄挂掉(不能排除这种可能性),很容易成为单点故障,继而影响到你的业务流程。如果是Redis集群,即使是微服务也一样会受到单点故障的影响

这种第三方库生成uuid,肯定不适合

穿梭于搜索内容中时我看到了csdn上的一篇关于多线程通过ThreadLocalRandom生成的随机UUID:https://blog.csdn.net/sinat_27143551/article/details/106981621

这位博主所介绍的方法:

import java.util.concurrent.ThreadLocalRandom;
 
public class ThreadLocalRandomDemo {
 
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Player().start();
        }
    }
 
    private static class Player extends Thread {
        @Override
        public void run() {
            System.out.println(getName() + ": " + ThreadLocalRandom.current().nextInt(100));
        }
    }
}

我看完这篇文章后灵感一现,想到了一种可行的方法

UUID = 随机UUID + 线程ID

当然这种方式是最简单的

实现代码:

     /**
     * 生成不重复的UUID,多线程下保证不重复
     */
    public static String getUUIDByThread(){
        return UUID.randomUUID().toString().replace("-", "") + Thread.currentThread().getId();
    }

经过万级数据多次测试,这种方式能够有效杜绝重复UUID的生成

当然还想到了一个相对复杂的方式,是上方方式的延续

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

实现方法:

对每台Web容器的JAVA_OPTIONS配置不一样的实例名

以Tomcat(8.0.53)为例,在startup.bat里配置

rem to set JAVA_OPTS
set "JAVA_OPTS=%JAVA_OPTS% -Dinstance.name=chuqiyun"

这样instance.name,就变成了JVM里的1个参数了

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部分组成:

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

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

这是我最近写程序遇到的一个折磨我2天的坑了,为了测试数据和查阅各类文档,博客,消耗了很多时间,但是结果确实挺成功的!

如果觉得本文对您有所帮助,可以支持下博主,一分也是缘
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇