java uuid多少位_UUID那些事儿

d63ace5be4ad22e05279984f98079e29.png

前言

很多人对UUID处于一种既熟悉又陌生的状态:“熟悉“是因为给实体(请求,事件等)分配一个唯一标识,与其他实体相区分,是很常见的使用场景。“陌生”是指能明白讲清楚UUID的生成逻辑和选取理由的少之又少。阅读下文之前,不妨思考如下几个问题:

  • 在一个分布式系统中,如何生成全局唯一的uuid?
  • 如何生成时间序的UUID?
  • UUID的版本是什么意思,version1和version4有什么区别?

引子

当两台机器需要相互交换信息的时候,他们就需要各自有一个唯一的标识符(名字)。历史上第一个可称为网络(network)的东西是建于1870s的电话网络,在此之前,电话通信完全是点对点的,这样的网络无意是昂贵、低效、不方便的(见题图,光是铜线的消耗就很可观)。

交换机的出现是电话史上的一个重大发明,每家只需要一条电话线连接到电话局,由它转接到目标电话(从网状拓扑变成星型图谱),由此带来了每部电话的唯一标识符——电话号码。

进入计算机时代之后,人们发现实体(Entity)这个概念的粒度一下子变小了:从物理实体(电话号码),提升到了逻辑概念(数据)。数据不只在一个地方,而是会在网络节点内部流转,需要持久化的保存。带来对UUID的根本需求:对业务域内(订单,数据库记录)的所有实体(通常关系上是平等的)赋予可相互区别的标识符(唯一性,不重复)。评估一个UUID算法的算法有如下角度:

  • 唯一性:在取值空间内的碰撞概率有多大。最重要的属性。
  • 随机性(可选):生成的UUID分布是否均匀
  • 有序性(可选):是否有拓展的属性能力,比如时间上有先后顺序的两个uuid在字典序上也有一样的顺序。
  • 隐蔽性(可选):是否会额外暴露内部的信息,比如某些用mac地址或者ip计算uuid的算法,就存在暴露内部信息的风险。用数字做订单的唯一标识符,则会让竞争对手估算出商业数据。
  • 依赖性(可选):每个节点生成uuid时候是否依赖其他中心化节点,相互之间是否要沟通。

标准定义

UUID的标准定义来自IETF的RFC 4122,UUID是由一组32位数的16进制数字所构成,故UUID理论上的总数为16^32=2^128,约等于3.4 x 10^38。也就是说若每纳秒产生1兆个UUID,要花100亿年才会将所有UUID用完。具体实现定义了五个版本,详见wiki,其中常用的是版本1(时间序)和版本4(随机)。其他版本2是版本1的DCE安全版本,被很多UUID实现忽略。版本3和版本5是基于现有名称映射(前者是散列,后者是SHA1),需要名称本身就具有唯一性(如URI)。这里有个网站:https://uuidonline.com/,可以生成各个版本的uuid找找感觉。

UUID的标准型式包含32个16进制数字,以连字号分为五段,形式为8-4-4-4-12的32个字符(不算连字符)。示例:

123e4567-e89b-12d3-a456-426655440000 xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx

第三组数字开头的M表示UUID版本,第四组数字开头的N的1到3个最高有效位表示UUID变体(同一版本的,用来解释某些位),上面例子M是1,N是a(10xx),以为他是按照版本1变体1的逻辑生成的。

具体实现上,版本1通过在高位引入时间信息,带来uuid的时间顺序,在低位引入位置信息(网卡mac地址),将碰撞的可能通过机器进行隔离。

时间戳说明:
timestamp由系统时钟获得,形式为60bit的整数,表示从1582/10/15 00:00:00至今经过的百纳秒数(100 ns= 1e-7 s)。
将unix timestamp换算为所需时间戳的公式为:ts * 10000000 + 122192928000000000
time_low = (long long)timestamp [32:64) ,将最低位的32bit按照同样的顺序填入UUID前32bit
time_mid = (long long)timestamp [16:32) ,将中间的16bit按照同样的顺序填入UUID的time_mid
time_high = (long long)timestamp [4:16) ,将最高的12bit按照同样的顺序生成time_hi。
time_hi和version是共享一个short int的,所以其生成方法为:
time_hi_and_version = (long long)timestamp[0:16) & 0x0111 | 0x1000 

UUID的第一个分组位域宽度为32bit,以百纳秒表示时间的话,也就是(2 ^ 32 / 1e7 s = 429.5 s = 7.1 min)。即每7分钟,第一个分组经历一次重置循环。所以对于随机到达的请求,生成的ID哈希分布应该是很均匀的。

UUID的第二个分组位域宽度为16bit,也就是2^48 / 1e7 s = 326 Day,也就是说,第二个分组基本上每年循环一次。可以近似的看做年内的业务日期。

UUID与JAVA实现

java使用这面临一个比较尴尬的现状:JDK里目前只有版本4的实现,如果需要使用其他的版本,可以参考JUG开源实现

https://github.com/cowtowncoder/java-uuid-generator​github.com
题外话:了解JAVA的知道class里有个serialVersionUID,他用在序列化和发序列化的场景区别用的是不是一个版本:如果反序列化的时候发现UID与序列化的UID不一样,则会抛出一个异常InvalidClassException。当你修改了这个类,然后认为是向前兼容的(如增加一个新属性),则不用修改UID的值。反之则要修改该值(如删除或修改了原属性)。
serialVersionUID如果没有显示生成,虚拟机会按照类名属性自己生成一个,不同虚拟机的实现方案不一样,所以可能同一个类在接收和发送端生成的UID不一样导致反序列化失败,故建议所有序列化的类都由用户显示生成这个值。

分布式实现

业界公司并不满足标准实现,对twitter来说,一个现实的需求就是不需要经过中心化节点,就可以对一组推文,按照时间序列进行排序(roughly sorting,只要时间相近的推文在一起即可)。脱离UUID标准定义之后,可以更加高效的利用每一个比特位。这就是snowflake。

K-ordering:Roughly Sorting的一种更专业的叫法。一个K-ordered Array的K值,来自其中所有元素,距离其正确位置的差距最大不超过K。举例来说:
数组 A=[1 4 2 6 3 7 5 8]
其正确的排序位置是 A=[1 2 3 4 5 6 7 8]
正确元素与其实际位置的差值 [0 1 2 2 2 2 1 0] ,故这个数组的K=2

基于这个思路,后续推出了flake,克服了snowflake只有64bit,同时依赖三方服务如zookeeper的问题。可惜是erlang实现,网上有按照其思路的java实现。

参考资料

  • Wiki: Universally unique identifier: https://en.wikipedia.org/wiki/Universally_unique_identifier
  • A brief history of the uuid:https://segment.com/blog/a-brief-history-of-the-uuid/
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值