最近正在写一个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的生成。。。
- 先看UUID各版本的实现原理:Universally unique identifier
- 再看JDK的实现(只实现了UUID的1,3,4版本):java.util.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天的坑了,为了测试数据和查阅各类文档,博客,消耗了很多时间,但是结果确实挺成功的!
好