1.JDK标准API
要生成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.数据库获取UUID
通过这种消耗大量性能来获取UUID,当然可行,但在高并发的场景下你真的会去考虑吗?
3.分布式UUID的生成
分布式?多台Web容器(我们可以称之为实例)在同1个机器(mac地址相同)下?不依赖第3方工具?最好在JVM解决?
思路
确保每台实例具有唯一的名字(我们可以称之为实例名)
确保某台实例生成的UUID不会重复: 当前系统时间 + 递增的数值(避免高并发的影响)
因此,算法如下:
UUID = 实例名 + 当前系统时间毫秒数 + 递增的int数
方法
对每台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个参数了
代码实现
package java.main; 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.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 null; } 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的长度:
- 13: System.currentTimeMillis()的长度是13位
- 11: Integer.MIN_VALUE的长度。Int值递增,达到Int的上限后,会从负数重新计数,因此长度是11位
- 2 * 实例名的字符数。实例名一般由字母、数字、小数点、减号、下划线组成,这些字符的ASCII码值是2位
如果这个UUID需要持久化,持久化的字段可定义成VARCHAR2(255),其中实例名的字符数最大可以是115 = ( 255 - 13 - 11 ) / 2