理解UUID们

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有两个重点:一是理解其组成部分;二是理解各版本对各部分的填充方式。我们先看最重要的——组成部分。

为了较为形象地展示,我画了张图。第一行是结果,第二行是十六进制说明,第三行是二进制说明。

image-20210930084244949

  • 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生成逻辑

  1. 获取一个系统级别的全局时钟

  2. 从一个系统全局共享的的存储位置,读取上一个UUID的状态:时间戳、始终序列、node等

  3. 获取当前时间戳:从UTC时间1582-10-15 00:00:00起,100ns的个数

  4. 获取nodeid,即MAC地址

  5. 如果上一个UUID状态不稳定(不存在、nodeid与新获取的nodeid不一样),生成一个随机clock value

  6. 如果状态存在,但时间戳比当前时间戳还晚,则clock sequence自增

  7. 将新的状态保存

  8. 将上面的三个部分按照格式组成UUID

有一个bug

MAC地址直接放在nodeid中,就是一个bug,这会暴露用户的MAC地址:梅丽莎病毒制作者的位置就是这么暴露的

Version 4生成逻辑

  1. 获取一个随机数
  2. 将预留位、版本位之外的位,使用该随机数填充,填充位对应方式,参考RFC

Version 3、5生成逻辑

  1. 命名空间+名字组成字符串,使用MD5或者SHA1计算摘要
  2. 将预留位、版本位之外的位,使用该摘要填充,填充位对应方式,参考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) {
   
   
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值