35. 通用唯一识别码UUID

先来看一个问题:假设一个网络系统每秒钟需要保存数十万来自用户提交的信息,并分配一个id给每条信息用于以后唯一标识它,那么怎么产生这个id呢?不能重复又要足够快以支持高并发,有这么强大的单台服务器吗?即便有,随着并发加大也是很难满足的,看来从设计上不能依赖于单台服务器。此外有些信息对象会跨系统存在,由于业务原因,需要进行全局唯一标识,比如一个业务单号,业务只关注这个业务单本身而不关注当前是哪台服务器在处理这个业务;这些就是uuid存在的原因。

UUID:通用唯一识别码Universally Unique IDentifier,有时也被称为GUID全局唯一识别码Globally Unique IDentifier,最初用于阿波罗网络计算系统,后来开源软件基金会用于许多分布式计算环境中,也用在微软window平台里,它是一个用以解决跨时空唯一性标识问题的东西,所谓跨时空表示在有限的时间和空间内,能够保证产生的id是独一无二的,这样一来就能解决上文提到的问题,但这里有个关键词“保证”需要注意,其实没有绝对的东西,只要概率极其低那么我们可以“保证”,这对于处女座的人来说可能是一种煎熬,但愿看完UUID机制会得到安慰,接下来看一下UUID如何做到概率极其低,到底有多低。

UUID参考:

关于UUID的规则在rfc4122中描述,详情见:

http://www.rfc-editor.org/rfc/rfc4122.txt

关于这篇RFC有一个勘误,是关于字节序的,见:

http://www.rfc-editor.org/errata_search.php?rfc=4122&eid=3546

UUID:

UUID是一个有128比特的位串(16字节),以十六进制字符的方式来表示,格式为8-4-4-4-12

正则描述为:'[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}'

如:34415db0-ed84-48e5-8aae-9d7e03bb45e9

这128比特的位串具备一定的格式,从第一位顺序描述如下:

time_low :4字节32位,表示时间的低字段

time_mid :2字节16位,表示时间的中字段

time_hi_and_version:2字节16位,前4位表示uuid创建类型的版本(见后),其余位表示时间的高字段

clk_seq_hi_res:1字节8位,前2到3位表示uuid版本变体(见后),其余位表示时钟序列高字段

clk_seq_low:1字节8位,表示时钟序列低字段

node:6字节48位,表示空间上的识别标识

 

uuid的设计目的是去中心化,不论世界上哪台计算机在什么时间产生的uuid需是唯一的(实际上是重复概率极其低),从上面可以看到有60位来表示时间,精度是100纳秒级别,目前服务器程序运行速度大多在毫秒级别,在同一台计算机上同一程序可以保证不停生成uuid也是不同的(在rfc4122里面也描述了超高频率获取uuid的解决办法),但这里有一个问题,计算机的时间可能重复,比如有些设备掉电后会丢失时间,这样一来产生相同uuid的几率就变大了,所以引入时钟序列字段克服这个问题,在系统初始化时时钟序列字段应该是一个随机值,解决了时间上可能相同的问题,再解决空间上相同的问题,node有48位,典型的它可以是网卡的MAC地址、IP地址、主机名、url等,唯一标识一台设备的标识,以此避免空间上的重复。

有时候软件不能获取到时间或者设备标识,那怎么办呢,可以采用随机数的方式,那么如何识别以何种方式创建的uuid?这就是前文提到的uuid创建类型版本字段的作用,它有4位,目前定义了5种创建方式,网上摘录如下:

UUID Version 1:基于时间的UUID

基于时间的UUID通过计算当前时间戳、随机数和机器MAC地址得到。由于在算法中使用了MAC地址,这个版本的UUID可以保证在全球范围的唯一性。但与此同时,使用MAC地址会带来安全性问题,这就是这个版本UUID受到批评的地方。如果应用只是在局域网中使用,也可以使用退化的算法,以IP地址来代替MAC地址--Java的UUID往往是这样实现的(当然也考虑了获取MAC的难度)。

UUID Version 2:DCE安全的UUID

DCE(Distributed Computing Environment)安全的UUID和基于时间的UUID算法相同,但会把时间戳的前4位置换为POSIX的UID或GID。这个版本的UUID在实际中较少用到。

UUID Version 3:基于名字的UUID(MD5)

基于名字的UUID通过计算名字和名字空间的MD5散列值得到。这个版本的UUID保证了:相同名字空间中不同名字生成的UUID的唯一性;不同名字空间中的UUID的唯一性;相同名字空间中相同名字的UUID重复生成是相同的。

UUID Version 4:随机UUID

根据随机数,或者伪随机数生成UUID,实际中往往使用密码学上的强随机数生成器。

UUID Version 5:基于名字的UUID(SHA1)

和版本3的UUID算法类似,只是散列值计算使用SHA1(Secure Hash Algorithm 1)算法。

前文还提到clk_seq_hi_res中有两到三位的版本变体Variant字段,它是什么意思呢?可以理解为uuid字段布局的版本号,在uuid中全部字段位的含义都依赖于这个字段值的设置,相当于是uuid机制的版本号,以上解释的字段含义目前定为10,只用了两个位;Variant字段的其他值或为应对未来变化而保留,或为兼容性考虑。

通过上面的介绍,除开创建版本的4位和变体字段的两位,还有122位,这是一个很大的数字(5后面加36个零),系统产生相同uuid的概率是非常非常小的,这个是一个什么概念呢,形象的说:

中彩票头奖的概率是千万分之一级别,假设每秒买一张彩票,一直买10的21次方年,全都中头奖,可能发生这样的事情吗?

或者1千亿台计算机,每台每秒产生1亿个uuid,需要一百六十亿年才会重复

这样的概率就是uuid重复的概率,是否能让处女座的人们安心的认为uuid能担保不重复呢?

drupal中的uuid:

在drupal中实体用到了uuid,如果网站规模做的很大时,会涉及数据库分表分库,此时uuid将带来帮助,系统中uuid由以它命名的uuid服务提供,如下:

容器服务id:uuid

类:Drupal\Component\Uuid\Php

使用方式:echo \Drupal::service("uuid")->generate();

程序代码解释如下:

class Php implements UuidInterface {
  public function generate() {
    // 使用密码级别的随机数生成器产生高质量随机性,将二进制转为16进制,$hex有32个字符
    $hex = bin2hex(Crypt::randomBytes(16));
    // 一个字节有两个十六进制字符,提取8个十六进制字符,4字节32位比特,表示时间低字段
    $time_low = substr($hex, 0, 8);
    //两字节表示时间中字段
    $time_mid = substr($hex, 8, 4);
    //此处字符4(不是数字)表示uuid创建版本为4,意指使用随机数生成,占用4比特位
    $time_hi_and_version = '4' . substr($hex, 13, 3);
    // 提取8比特位,并转换为十进制整数
    $clock_seq_hi_and_reserved = base_convert(substr($hex, 16, 2), 16, 10);
    //将前两位设置为0
    $clock_seq_hi_and_reserved &= 0b00111111;
    //将前两位设置为10
    $clock_seq_hi_and_reserved |= 0b10000000;
    //提取1字节(8位)做时钟序列低位
    $clock_seq_low = substr($hex, 18, 2);
    //余下的6字节48位作为设备标识
    $node = substr($hex, 20);
    //格式化输出
    $uuid = sprintf('%s-%s-%s-%02x%s-%s',
      $time_low, $time_mid, $time_hi_and_version,
      $clock_seq_hi_and_reserved, $clock_seq_low,
      $node
    );
    return $uuid;
  }
}

 

可以看出uuid的第三段的第一个字符始终为4,是因为drupal使用随机数的方式来生成uuid,既没有用到时间也没有用到设备标识,第四段的第一个字符为二进制10**表示的值,那代表目前uuid的版本类型

在drupal中uuid是以组件方式提供,同时还提供了其他几种生成方式,但需要扩展支持

如果需要验证一个uuid可使用:

Drupal\Component\Uuid\Uuid:: isValid($uuid);

返回bool值,表示是否符合uuid的特征。

本书共91小节:

评论 (写第一个评论)