UUID简介

定义

UUID 是 通用唯一识别码(Universally Unique Identifier)的缩写,目的是让分布式系统中的所有元素,都有唯一辨识,而不需要通过中央控制端来做辨识指定。
由算法机器生成。为保证UUID的唯一性,规范定义了包括网卡MAC地址、时间戳、名字空间(Namespace)、随机或伪随机数、时序等元素,以及从这些元素生成UUID的算法。UUID的复杂特性在保证了其唯一性的同时,意味着只能由计算机生成。
非人工指定,非人工识别。UUID是不能人工指定的,除非你冒着UUID重复的风险。UUID的复杂性决定了“一般人“不能直接从一个UUID知道哪个对象和它关联。

版本

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

Version 1

基于时间的UUID。通过计算当前时间戳、随机数和机器MAC地址得到,由于在算法中使用了MAC地址,这个版本的UUID可以保证在全球范围的唯一性。但与此同时,使用MAC地址会带来安全性问题。如果应用只是在局域网中使用,也可以使用退化的算法,以IP地址来代替MAC地址。

Version 2

DCE(Distributed Computing Environment)安全的UUID。和基于时间的UUID算法相同,但会把时间戳的前4位置换为POSIX的UID或GID。

Version 3

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

Version 4

随机UUID。根据随机数,或者伪随机数生成UUID。这种UUID产生有一定的重复概率但是极低。

Version 5

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

应用

Version 1/2适合应用于分布式计算环境下,具有高度的唯一性。
Version 3/5适合于一定范围内名字唯一,且需要或可能会重复生成UUID的环境下。
Version4在数据量亿级以下或者唯一性要求不严谨的情况下使用。
Java中UUID提供了两个方法:randomUUID()和nameUUIDFromBytes(byte[] name),分别对应Version4和Version3。jdk实现如下

1
2
3
4
5
6
7
8
9
10
11
public static UUID randomUUID() {
SecureRandom ng = Holder.numberGenerator;
byte[] randomBytes = new byte[16];
ng.nextBytes(randomBytes);
randomBytes[6] &= 0x0f; /* clear version */
randomBytes[6] |= 0x40; /* set to version 4 */
randomBytes[8] &= 0x3f; /* clear variant */
randomBytes[8] |= 0x80; /* set to IETF variant */
return new UUID(randomBytes);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static UUID nameUUIDFromBytes(byte[] name) {
MessageDigest md;
try {
md = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException nsae) {
throw new InternalError("MD5 not supported", nsae);
}
byte[] md5Bytes = md.digest(name);
md5Bytes[6] &= 0x0f; /* clear version */
md5Bytes[6] |= 0x30; /* set to version 3 */
md5Bytes[8] &= 0x3f; /* clear variant */
md5Bytes[8] |= 0x80; /* set to IETF variant */
return new UUID(md5Bytes);
}

扩展

减小Version4重复概率

System.currentTimeMillis() + “_” + UUID.randomUUID().toString()生成结果示例:

1
1530454044000_bec863fc-92a4-4c62-8156-7ef9c0169abc

可大幅度减少Version4的重复概率,缺点是拼接字符串后的值太长,长度为13(时间戳长度) + 1(连接字符) + 36(32位实际UUID加4位连接字符) = 50。

唯一id其他方案

以订单Id的解决方案为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import org.joda.time.DateTime;
import java.text.DecimalFormat;
public class IdGenerator {
/**
* 毫秒内生成的最大序列值
*/
private static final long maxSequence = 999;
/**
* 毫秒内序列,0~999
*/
private static long sequence = 0L;
/**
* 上次生成ID的时间截
*/
private static long lastTimestamp = -1L;
/**
* 获得下一个ID (线程安全)
* 订单生成规则 2位payType + 5位appId + 17位年月日时分秒 + 1位机器id + 3位毫秒序列值
*
* @param payType 支付类型
* @param appId 应用id
* @param machineId 机器id
* @return id 新Id
*/
public synchronized static String nextId(int payType, int appId, int machineId) {
long timestamp = timeGen();
//如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
if (timestamp < lastTimestamp) {
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
//如果是同一时间生成的,则进行毫秒内序列
if (lastTimestamp == timestamp) {
sequence = sequence + 1;
//毫秒内序列溢出
if (sequence > maxSequence) {
//阻塞到下一个毫秒
timestamp = tilNextMillis(lastTimestamp);
sequence = 0L;
}
} else { //时间戳改变,毫秒内序列重置
sequence = 0L;
}
//上次生成ID的时间截
lastTimestamp = timestamp;
String id = new StringBuilder()
.append(new DecimalFormat("00").format(payType))
.append(new DecimalFormat("00000").format(appId))
.append(new DateTime(timestamp).toString("yyyyMMddHHmmssSSS"))
.append(machineId)
.append(new DecimalFormat("000").format(sequence))
.toString();
return id;
}
/**
* 阻塞到下一个毫秒,直到获得新的时间戳
*
* @param lastTimestamp 上次生成ID的时间截
* @return 当前时间戳
*/
private static long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
/**
* 返回以毫秒为单位的当前时间
*
* @return 当前时间戳(毫秒)
*/
private static long timeGen() {
return System.currentTimeMillis();
}
}

当前代码所在服务为单点服务且数据库唯一,数据库订单Id约束为unique。
若改为集群服务,数据库仍为单点,代码不变的情况下,可通过数据库异常保证订单Id的唯一性。若不通过异常排除,分布式下更好的方案为使用缓存如Redis或将订单Id的生成封装为单个服务。