为什么需要重新实现一个UUID
JVM自带版本实现了UUID的全局唯一性,它使用网卡MAC地址计算UUID机器位,并且使用了摘要算法使其结果足够随机。但有时候实际项目的需求有所不同,UUID的随机性反而增加了调试与维护时的麻烦,我们仅仅需要一个全局唯一的UUID,并且需要能够按时间有序。
实现思路
目标
- 全局唯一性
- 有序性
全局唯一性
对于全局唯一性要求,我们仅需分步实现以下两点即可:
-
同JVM实例下,ID唯一
-
多JVM实例下,ID唯一
首先,考虑实现同JVM实例下ID的唯一性:
考虑到唯一性,我们可能可以想到最原始的计数器,每生成一个ID,值自增1,只需要简单的线程安全级别的计数器即可,当然考虑到ID是全局使用,得考虑容量需要足够大至少需要long类型,因此AtomicLong
是个很好的选择。很好,这跟Mysql的自增ID是一样的了,既实现了唯一性又满足了有序性,但是接下来就会发现有一个问题,Mysql是长时间不关机的并且使用文件存储数据的,而JVM随时可能关机重启,重启后上一次计数到哪个数值就不知道了,需要保证重启后仍然唯一是个大问题。
这时候就得考虑将计数器换成一个不受开关机影响的因子。很快就能想到了生成ID的时间,不管是开机还是关机系统时间总是一直往前的,只要与生成ID的时间挂钩同样能够提供唯一性,并且保证重启后生成的ID不与重启前生成的ID重复,同时又能顺带满足有序性的要求。时间虽然是连续且唯一的概念,但是系统中的时间却是精度有限的,精确到毫秒级(System.currentTimeMillis()
),同一时间有可能生成多个ID,因此还需要保证同一毫秒内同一实例生成的的ID不重复。
Tip:虽然对于时间有粒度更细的System.nanoTime()
可以精确到纳秒,但System.nanoTime()
是以启动时为0秒计算的,并不能保证重启后不重复
不过既然已经控制到毫秒级别了,这就很简单了,只要重新在每一毫秒内加上计数器即可,由于一毫秒的时间足够短,能生成的ID数量有限,因此只需要小容量的线程安全级别的计数器(AtomicInteger
)即可,同一毫秒内每生成一个ID,计数器递增。这样毫秒级别时间戳加上一个计数器,我们达到了同JVM实例下ID唯一的目标。
接下来,考虑多JVM实例下ID的唯一性
既然同一JVM下ID已经唯一了,那么给ID再加上一个实例唯一的标识不就行了吗。思路很明显了,只需要给实例自动生成唯一的标识符即可。机器上能够提供唯一性的元素很多,比如MAC地址,IP地址,当然这些也是相对唯一的,在特定的情境下可以认为是唯一的。由于自己确定的项目使用,也不需要太复杂的方案,选取了一个简单易获取的元素——IP地址,考虑到仅仅IP地址仍旧不够保险,再三考虑后增加了一个元素——类初始化时间——与IP共同构成了JVM标识。很好,到这里就满足了多JVM实例下ID的唯一性了。
有序性
为了保证ID的唯一性,已经确定选取了4个元素作为ID的组成部分:IP地址、类初始化时间、生成ID的时间、计数器。再需要保证有序性,只需要调整ID组成元素部分的顺序即可,由于生成ID的时间是保证有序性的关键,因此需要将其放置第一个部分,这样即保证了毫秒级别保持顺序。
优化
ID不希望太长 从字符集方面进行优化,优化字符集到base32(由于UUID仅使用base16编码,使其即使长度达到了32位,携带的信息却并不多),优化到base32字符集后,虽然携带的信息量多了不少,但长度仍然仅为32位
IP做为ID一部分存在少量信息暴露的考虑 从IP匿名化和字符集多样化方面进行优化:1.通过IP替换方式来变更IP部分单元,2.IP地址部分使用独立字符集编码,不同项目可根据需要单独修改掉该字符集
自定义IP需求 从环境变量机制和SPI机制方面进行优化:1.从环境变量传递参数替换部分IP单