UUID介绍与生成方法

什么是UUID?

UUID是Universally Unique Identifier的缩写,它是在一定的范围内(从特定的名字空间到全球)唯一的机器生成的标识符。UUID具有以下涵义:
 

  • 经由一定的算法机器生成

为了保证UUID的唯一性,规范定义了包括网卡MAC地址、时间戳、名字空间(Namespace)、随机或伪随机数、时序等元素,以及从这些元素生成UUID的算法。UUID的复杂特性在保证了其唯一性的同时,意味着只能由计算机生成。

  • 非人工指定,非人工识别

UUID是不能人工指定的,除非你冒着UUID重复的风险。UUID的复杂性决定了“一般人“不能直接从一个UUID知道哪个对象和它关联。

  • 在特定的范围内重复的可能性极小

UUID的生成规范定义的算法主要目的就是要保证其唯一性。但这个唯一性是有限的,只在特定的范围内才能得到保证,这和UUID的类型有关(参见UUID的版本)。

UUID是16字节128位长的数字,通常以36字节的字符串表示,示例如下:

3F2504E0-4F89-11D3-9A0C-0305E82C3301

其中的字母是16进制表示,大小写无关。
 

Universally Unique IDentifier(UUID),有着正儿八经的RFC规范,是一个128bit的数字,也可以表现为32个16进制的字符,中间用”-”分割。

- 时间戳+UUID版本号,分三段占16个字符(60bit+4bit),
- Clock Sequence号与保留字段,占4个字符(13bit+3bit),
- 节点标识占12个字符(48bit),

GUID(Globally Unique Identifier)是UUID的别名;但在实际应用中,GUID通常是指微软实现的UUID。



UUID的版本

UUID具有多个版本,每个版本的算法不同,应用范围也不同。

首先是一个特例--Nil UUID--通常我们不会用到它,它是由全为0的数字组成,如下:

00000000-0000-0000-0000-000000000000



UUID Version 1:基于时间的UUID

因为时间戳有满满的60bit,所以可以尽情花,以100纳秒为1,从1582年10月15日算起(能撑3655年,真是位数多给烧的,1582年有意思么)

节点标识也有48bit,一般用MAC地址表达,如果有多块网卡就随便用一块。如果没网卡,就用随机数凑数,或者拿一堆尽量多的其他的信息,比如主机名什么的,拼在一起再hash一把。

顺序号这16bit则仅用于避免前面的节点标示改变(如网卡改了),时钟系统出问题(如重启后时钟快了慢了),让它随机一下避免重复。

但好像Version 1就没考虑过一台机器上起了两个进程这类的问题,也没考虑相同时间戳的并发问题,所以严格的Version1没人实现,接着往下看各个变种吧。

 

Version1变种 – Hibernate

Hibernate的CustomVersionOneStrategy.java,解决了之前version 1的两个问题

- 时间戳(6bytes, 48bit):毫秒级别的,从1970年算起,能撑8925年….
- 顺序号(2bytes, 16bit, 最大值65535): 没有时间戳过了一秒要归零的事,各搞各的,short溢出到了负数就归0。
- 机器标识(4bytes 32bit): 拿localHost的IP地址,IPV4呢正好4个byte,但如果是IPV6要16个bytes,就只拿前4个byte。
- 进程标识(4bytes 32bit): 用当前时间戳右移8位再取整数应付,不信两条线程会同时启动。

值得留意就是,机器进程和进程标识组成的64bit Long几乎不变,只变动另一个Long就够了。

 

Version1变种 – MongoDB

MongoDB的ObjectId.java

- 时间戳(4 bytes 32bit): 是秒级别的,从1970年算起,能撑136年。

- 自增序列(3bytes 24bit, 最大值一千六百万): 是一个从随机数开始(机智)的Int不断加一,也没有时间戳过了一秒要归零的事,各搞各的。因为只有3bytes,所以一个4bytes的Int还要截一下后3bytes。

- 机器标识(3bytes 24bit): 将所有网卡的Mac地址拼在一起做个HashCode,同样一个int还要截一下后3bytes。搞不到网卡就用随机数混过去。

- 进程标识(2bytes 16bits):从JMX里搞回来到进程号,搞不到就用进程名的hash或者随机数混过去。

可见,MongoDB的每一个字段设计都比Hibernate的更合理一点,比如时间戳是秒级别的。总长度也降到了12 bytes 96bit,但如果果用64bit长的Long来保存有点不上不下的,只能表达成byte数组或16进制字符串。

另外对Java版的driver在自增序列那里好像有bug。

 

Twitter的snowflake派号器

snowflake也是一个派号器,基于Thrift的服务,不过不是用redis简单自增,而是类似UUID version1,

只有一个Long 64bit的长度,所以IdWorker紧巴巴的分配成:

- 时间戳(42bit) 自从2012年以来(比那些从1970年算起的会过日子)的毫秒数,能撑139年。
- 自增序列(12bit,最大值4096), 毫秒之内的自增,过了一毫秒会重新置0。
- DataCenter ID (5 bit, 最大值32),配置值。
- Worker ID ( 5 bit, 最大值32),配置值,因为是派号器的id,所以一个数据中心里最多32个派号器就够了,还会在ZK里做下注册。

可见,因为是派号器,把机器标识和进程标识都省出来了,所以能够只用一个Long表达。

另外,这种派号器,client每次只能一个ID,不能批量取,所以额外增加的延时是问题。

 

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产生重复的概率是可以计算出来的,但随机的东西就像是买彩票:你指望它发财是不可能的,但狗屎运通常会在不经意中到来。

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

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

UUID的应用

从UUID的不同版本可以看出,Version 1/2适合应用于分布式计算环境下,具有高度的唯一性;Version 3/5适合于一定范围内名字唯一,且需要或可能会重复生成UUID的环境下;至于Version 4,我个人的建议是最好不用(虽然它是最简单最方便的)。

通常我们建议使用UUID来标识对象或持久化数据,但以下情况最好不使用UUID:

  • 映射类型的对象。比如只有代码及名称的代码表。
  • 人工维护的非系统生成对象。比如系统中的部分基础数据。

对于具有名称不可重复的自然特性的对象,最好使用Version 3/5的UUID。比如系统中的用户。如果用户的UUID是Version 1的,如果你不小心删除了再重建用户,你会发现人还是那个人,用户已经不是那个用户了。(虽然标记为删除状态也是一种解决方案,但会带来实现上的复杂性。)

UUID生成器

我没想着有人看完了这篇文章就去自己实现一个UUID生成器,所以前面的内容并不涉及算法的细节。下面是一些可用的Java UUID生成器:
 

  • Java UUID Generator (JUG):开源UUID生成器,LGPL协议,支持MAC地址。
  • UUID:特殊的License,有源码。
  • Java 5以上版本中自带的UUID生成器:好像只能生成Version 3/4的UUID。

此外,Hibernate中也有一个UUID生成器,但是,生成的不是任何一个(规范)版本的UUID,强烈不建议使用。

 

生成方法

搜集了一些UUID的生成方法,整理如下

Shell

  • Unix/Linux环境中大都有一个名为uuidgen的小工具,运行即可生成一个UUID到标准输出

读取文件/proc/sys/kernel/random/uuid即得UUID,例如:

cat /proc/sys/kernel/random/uuid

libuuid

libuuid是一个用于生成UUID的C库,具体用法参考http://linux.die.net/man/3/libuuid,示例如下:

#include <stdio.h>
 
#include <uuid/uuid.h>
 
 
int main(int argc, char **argv)
{
 
uuid_t uuid;
 
char str[36];
 
uuid_generate(uuid);
 
uuid_unparse(uuid, str);
 
printf("%s\n", str);
 
return 0;
 
}

在Linux下编译时需要链接uuid库

gcc -o uuid uuid.c -luuid

在Ubuntu中,可以用下面的命令安装libuuid:

sudo apt-get install uuid-dev

boost uuid

Boost库是一个可移植的开源C++库,它提供了UUID的实现。

下面的代码可以生成一个UUID

#include <boost/uuid/uuid.hpp>
 
#include <boost/uuid/uuid_generators.hpp>
 
 
boost::uuids::uuid a_uuid = boost::uuids::random_generator();
 

Qt QUuid

Qt是一个跨平台的C++编程框架,QUuid类实现了UUID的生成、比较、转换等功能。

函数QUuid createUuid();可用于生成一个随即UUID。示例如下

#include <iostream>
 
#include <QUuid>
 
#include <QString>
 
 
int main()
 
{
 
QUuid uuid = QUuid::createUuid();
 
std::cout << qPrintable(uuid.toString()) << std::endl;
 
return 0;
 
}
 

CoCreateGuid

Windows下提供了函数CoCreateGuid用于生成GUID。需要使用的头文件是”objbase.h”,需要链接的库是ole32.lib,函数原型为:

HRESULT CoCreateGuid(GUID *pguid);

GUID的原型为

typedef struct _GUID
 
{
 
DWORD Data1;
 
WORD Data2;
 
WORD Data3;
 
BYTE Data4[8];
 
} GUID;
 

Java

JDK 1.5以上支持UUID,用法如下:

import java.util.UUID;
 
String uuid = UUID.randomUUID().toString();

 

 

展开阅读全文

没有更多推荐了,返回首页