UUID,Universal Unique Identifier,全局唯一标识符。也叫做GUID,Global Unique Identifier。
概念都了解,全局唯一嘛,但怎么实现的?多大概率重复?JDK的UUID和PostgreSQL的UUID一样吗?带着这些问题,我们从RFC,到JDK源码、PG手册,一点点看。
本文包含以下内容:
- UUID实现原理
- 自己实现一个UUID
- JDK的实现方式
- PG的实现方式
- 其它全局唯一ID
实现原理
老样子,要想了解一项基础技术,最好的方式是阅读一手资料。于UUID,它是RFC4122。
这一节,更莫如说是对RFC的总结,毕竟规范这东西,写得太啰嗦了,全文字不说,还没有示意图。
基本特性
- 长度128个bit位。一般通过16位十六进制字符表示,如:4cc9de16-414b-4f68-9b7e-6feeb8f629b0
- 不需要中心管理,具有跨越时间和空间的唯一性
- 按照本标准中的算法,支持每台机器每秒高达1000万次高分配速率
组成
理解UUID有两个重点:一是理解其组成部分;二是理解各版本对各部分的填充方式。我们先看最重要的——组成部分。
为了较为形象地展示,我画了张图。第一行是结果,第二行是十六进制说明,第三行是二进制说明。
- time-low:时间戳低位,占用32个bit
- time-mid:时间戳中位,占用16个bit
- time-high-and-version:时间戳高位+版本号,前者占12个bit,后者占4个bit,注意区分版本号是在前的
- clock-seq-high-and-reserved:时钟序列高位+预留位。前者占6个bit,后者占2个bit,也注意他们的前后顺序
- clock-seq-low:时间序列低位,占8个bit
- node:节点,占用48个bit
引出新概念,time、clock-seq、node
- time:即时间戳
- clock-seq:当时间戳或node重复时,使用clock-seq作为附加保证唯一性
- node:机器的节点,一般是机器的MAC地址
区分版本
注意到上面说组成时,有一个version字段。UUID是有多个版本的,目前总计5个。
版本规定了各字段的填充方式,版本2比较特殊这里忽略,其它版本如下
版本 | Timestamp | Clock sequence | node |
---|---|---|---|
1 | 从UTC时间1582-10-15 00:00:00起,100ns的个数 | 第一个clock sequence应该是随机产生的 如果知道本机上一次生成UUID的clock sequence,则此次只需要在其基础上加一 如果不知道,该字段需要设置成一个随机数 |
MAC地址 如果没有,则使用随机数 |
3 | 命名空间+名称的MD5只的一部分 | 命名空间+名称的MD5只的一部分 | 命名空间+名称的MD5只的一部分 |
4 | 随机数的一部分 | 随机数的一部分 | 随机数的一部分 |
5 | 命名空间+名称的SHA1只的一部分 | 命名空间+名称的SHA1只的一部分 | 命名空间+名称的SHA1只的一部分 |
可以看到,所谓的时间戳、时钟序列、node这些字段,仅对版本1有效,其它版本填充进去的值并无逻辑意义。
如何保证唯一性
- 对Version 1:它通过MAC地址保证空间唯一性,时间戳+序列号保证时间唯一性
- 对Version 2:和Version 1类似,只不过会把时间戳的前4位置换为POSIX的UID或GID
- 对Version 3、5:它纯依赖于名字保证唯一性,这就需要一个规整的命名系统:命名空间+名称
- 对Version 4:它纯通过随机数保证唯一性,此时一个高质量的随机数发生器就显得尤为重要
Version 1生成逻辑
-
获取一个系统级别的全局时钟
-
从一个系统全局共享的的存储位置,读取上一个UUID的状态:时间戳、始终序列、node等
-
获取当前时间戳:从UTC时间1582-10-15 00:00:00起,100ns的个数
-
获取nodeid,即MAC地址
-
如果上一个UUID状态不稳定(不存在、nodeid与新获取的nodeid不一样),生成一个随机clock value
-
如果状态存在,但时间戳比当前时间戳还晚,则clock sequence自增
-
将新的状态保存
-
将上面的三个部分按照格式组成UUID
有一个bug
MAC地址直接放在nodeid中,就是一个bug,这会暴露用户的MAC地址:梅丽莎病毒制作者的位置就是这么暴露的
Version 4生成逻辑
- 获取一个随机数
- 将预留位、版本位之外的位,使用该随机数填充,填充位对应方式,参考RFC
Version 3、5生成逻辑
- 命名空间+名字组成字符串,使用MD5或者SHA1计算摘要
- 将预留位、版本位之外的位,使用该摘要填充,填充位对应方式,参考RFC
为什么时间戳从1582-10-15开始
这是公历改革到基督教日历的日期,说来话长,我也没啥兴趣去详细了解,如果需要,看看知乎吧
自己写一个UUID吧
尝试着实现了一下抽象定义和基于时间戳的版本,发现主要有几个难点:kotlin的进制转换、二进制操作等。
这是一个不能实际使用的UUID版本(实现它也不是本文的目的),仅作演示。
先是UUID的抽象定义,我们使用两个Long作为底层bit持有对象,定义各字段的set方法,主要是二进制操作。
abstract class UUID {
companion object {
const val TIME_LOW_MASK = (0xFFFFFFFFL).shl(32)
const val TIME_MID_MASK = (0xFFFFFFFFL).shl(16)
const val VERSION_MASK = (0xFFL).shl(12)
const val TIME_HIGH_MASK = 0xFFFFFFL
const val RESERVED_MASK = (0xFL).shl(62)
const val CLOCK_SEQ_HIGH_MASK = (0xFFFL).shl(56)
const val CLOCK_SEQ_LOW_MASK = (0xFFFFL).shl(48)
const val NODE_MASK = 0xFFFFFFFFFFFFL
fun timeBasedUUID(): UUID = UUIDVersion1()
}
/**
* 高有效位们:靠右
*/
private var mostSignificantBits = 0L
/**
* 低有效位:靠左
*/
private var leastSignificantBits = 0L
fun setTimeLow(timeLow: Int) {
leastSignificantBits = leastSignificantBits.or(timeLow.toLong().shl(32).and(TIME_LOW_MASK))
}
fun setTimeMid(timeMid: Int) {