全局唯一ID,目的是让分布式系统中的所有元素都能有唯一的识别信息。
1.UUID
UUID概述
UUID (Universally Unique Identifier),通用唯一识别码。UUID是基于当前时间、计数器(counter)和硬件标识(通常为无线网卡的MAC地址)等数据计算生成的。
格式 & 版本
UUID由以下几部分的组合:
- 当前日期和时间,UUID的第一个部分与时间有关,如果你在生成一个UUID之后,过几秒又生成一个UUID,则第一个部分不同,其余相同。
- 时钟序列。
- 全局唯一的IEEE机器识别号,如果有网卡,从网卡MAC地址获得,没有网卡以其他方式获得。
UUID 是由一组32位数的16进制数字所构成,以连字号分隔的五组来显示,形式为 8-4-4-4-12,总共有 36个字符(即三十二个英数字母和四个连字号)。例如:
aefbbd3a-9cc5-4655-8363-a2a43e6e6c80
xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx
数字 M
的表示 UUID 版本,当前规范有5个版本,M可选值为1, 2, 3, 4, 5
;
数字 N
的一至四个最高有效位(bit)表示 UUID 变体( variant ),有固定的两位10xx
,因此N只可能取值8, 9, a, b
。
UUID版本通过M表示,当前规范有5个版本,M可选值为1, 2, 3, 4, 5
。这5个版本使用不同算法,利用不同的信息来产生UUID,各版本有各自优势,适用于不同情景。具体使用的信息
-
version 1, date-time & MAC address
基于时间的UUID通过计算当前时间戳、随机数和节点标识:机器MAC地址得到。由于在算法中使用了MAC地址,这个版本的UUID可以保证在全球范围的唯一性。但与此同时,使用MAC地址会带来安全性问题,这就是这个版本UUID受到批评的地方。同时, Version 1没考虑过一台机器上起了两个进程这类的问题,也没考虑相同时间戳的并发问题,所以严格的Version1没人实现,Version1的变种有Hibernate的CustomVersionOneStrategy.java、MongoDB的ObjectId.java、Twitter的snowflake等。
-
version 2, date-time & group/user id
DCE(Distributed Computing Environment)安全的UUID和基于时间的UUID算法相同,但会把时间戳的前4位置换为POSIX的UID或GID。这个版本的UUID在实际中较少用到。
-
version 3, MD5 hash & namespace
基于名字的UUID通过计算名字和名字空间的MD5散列值得到。这个版本的UUID保证了:相同名字空间中不同名字生成的UUID的唯一性;不同名字空间中的UUID的唯一性;相同名字空间中相同名字的UUID重复生成是相同的。
-
version 4, pseudo-random number
根据随机数,或者伪随机数生成UUID。
-
version 5, SHA-1 hash & namespace
和版本3的UUID算法类似,只是散列值计算使用SHA1(Secure Hash Algorithm 1)算法。
使用较多的是版本1和版本4,其中版本1使用当前时间戳和MAC地址信息。版本4使用(伪)随机数信息,128bit中,除去版本确定的4bit和variant确定的2bit,其它122bit全部由(伪)随机数信息确定。若希望对给定的一个字符串总是能生成相同的 UUID,使用版本3或版本5。
重复几率
Java中 UUID 使用版本4进行实现,所以由java.util.UUID类产生的 UUID,128个比特中,有122个比特是随机产生,4个比特标识版本被使用,还有2个标识变体被使用。利用生日悖论,可计算出两笔 UUID 拥有相同值的机率约为
p(n) ≈ 1 - e ^-n*n/2x^
其中x
为 UUID 的取值范围,n
为 UUID 的个数。
以下是以 x = 2^122^ 计算出n笔 UUID 后产生碰撞的机率:
n | 机率 |
---|---|
68,719,476,736 = 236 | 0.0000000000000004 (4 x 10^-16^) |
2,199,023,255,552 = 241 | 0.0000000000004 (4 x 10^-13^) |
70,368,744,177,664 = 246 | 0.0000000004 (4 x 10^-10^) |
产生重复 UUID 并造成错误的情况非常低,是故大可不必考虑此问题。
机率也与随机数产生器的质量有关。若要避免重复机率提高,必须要使用基于密码学上的强伪随机数产生器来生成值才行。
UUID 是由一组32位数的16进制数字所构成,是故 UUID 理论上的总数为16^32^ =2^128^,约等于3.4 x 10^123^。也就是说若每纳秒产生1百万个 UUID,要花100亿年才会将所有 UUID 用完。
Java实现
/**
* Static factory to retrieve a type 4 (pseudo randomly generated) UUID.
* 使用静态工厂来获取版本4(伪随机数生成器)的 UUID
* The {@code UUID} is generated using a cryptographically strong pseudo
* 这个UUID生成使用了强加密的伪随机数生成器(PRNG)
* random number generator.
*
* @return A randomly generated {@code UUID}
*/
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);
}
/**
* Static factory to retrieve a type 3 (name based) {@code UUID} based on
* the specified byte array.
* 静态工厂对版本3的实现,对于给定的字符串(name)总能生成相同的UUID
* @param name
* A byte array to be used to construct a {@code UUID}
*
* @return A {@code UUID} generated from the specified array
*/
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);
}
生成UUID
// Java语言实现
import java.util.UUID;
public class UUIDProvider{
public static void main(String[] args) {
// 利用伪随机数生成版本为4,变体为9的UUID
System.out.println(UUID.randomUUID());
// 对于相同的命名空间总是生成相同的UUID,版本为3,变体为9
// 命名空间为"xxx"时生成的UUID总是为f561aaf6-ef0b-314d-8208-bb46a4ccb3ad
System.out.println(UUID.nameUUIDFromBytes("xxx".getBytes()));
}
}
优点
- 简单,代码方便。
- 生成ID性能非常好,基本不会有性能问题。本地生成,没有网络消耗。
- 全球唯一,在遇见数据迁移,系统数据合并,或者数据库变更等情况下,可以从容应对。
缺点
- 采用无意义字符串,没有排序,无法保证趋势递增。
- UUID使用字符串形式存储,数据量大时查询效率比较低
- 存储空间比较大,如果是海量数据库,就需要考虑存储量的问题。
2.雪花算法(twitter/snowflake)
雪花算法概述
SnowFlake 算法,是 Twitter 开源的分布式 id 生成算法。其核心思想就是:使用一个 64 bit 的 long 型的数字作为全局唯一 id。在分布式系统中的应用十分广泛,且ID 引入了时间戳,基本上保持自增的。其原始版本是scala版,后面出现了许多其他语言的版本如Java、C++等。
格式
-
1bit - 首位无效符
-
41bit - 时间戳(毫秒级)
- 41位可以表示2^41^ -1个数字;
- 2^41^ -1毫秒,换算成年就是表示 69 年的时间
-
10bit - 工作机器id
- 5bit - datacenterId机房id
- 5bit - workerId机器 id
-
12bit - 序列号
序列号,用来记录同一个datacenterId中某一个机器上同毫秒内产生的不同id。
特点(自增、有序、适合分布式场景)
- 时间位:可以根据时间进行排序,有助于提高查询速度。
- 机器id位:适用于分布式环境下对多节点的各个节点进行标识,可以具体根据节点数和部署情况设计划分机器位10位长度,如划分5位表示进程位等。
- 序列号位:是一系列的自增id,可以支持同一节点同一毫秒生成多个ID序号,12位的计数序列号支持每个节点每毫秒产生4096个ID序号
snowflake算法可以根据项目情况以及自身需要进行一定的修改。
Twitter算法实现
Twitter算法实现(Scala)
Java算法实现
public class IdWorker{
//10bit的工作机器id
private long workerId; // 5bit
private long datacenterId; // 5bit
private long sequence; // 12bit 序列号
public IdWorker(long workerId, long datacenterId, long sequence){
// sanity check for workerId
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0",maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0",maxDatacenterId));
}
System.out.printf("worker starting. timestamp left shift %d, datacenter id bits %d, worker id bits %d, sequence bits %d, workerid %d",
timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId);
this.workerId = workerId;
this.datacenterId = datacenterId;
this.sequence = sequence;
}
//初始时间戳
private long twepoch = 1288834974657L;
//长度为5位
private long workerIdBits = 5L;
private long datacenterIdBits = 5L;
//最大值 -1 左移 5,得结果a,-1 异或 a:利用位运算计算出5位能表示的最大正整数是多少。
private long maxWorkerId = -1L ^ (-1L << workerIdBits); //31
private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); // 31
//序列号id长度
private long sequenceBits = 12L;
//序列号最大值
private long sequenceMask = -1L ^ (-1L << sequenceBits); //4095
//workerId需要左移的位数,12位
private long workerIdShift = sequenceBits; //12
//datacenterId需要左移位数
private long datacenterIdShift = sequenceBits + workerIdBits; // 12+5=17
//时间戳需要左移位数
private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits; // 12+5+5=22
//上次时间戳,初始值为负数
private long lastTimestamp = -1L;
public long getWorkerId(){
return workerId;
}
public long getDatacenterId(){
return datacenterId;
}
public long getTimestamp(){
return System.currentTimeMillis();
}
//下一个ID生成算法
public synchronized long nextId() {
long timestamp = timeGen();
//获取当前时间戳如果小于上次时间戳,则表示时间戳获取出现异常
if (timestamp < lastTimestamp) {
System.err.printf("clock is moving backwards. Rejecting requests until %d.", lastTimestamp);
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds",
lastTimestamp - timestamp));
}
//获取当前时间戳如果等于上次时间戳(同一毫秒内),则在序列号加一;否则序列号赋值为0,从0开始。
if (lastTimestamp == timestamp) {
// 通过位与运算保证计算的结果范围始终是 0-4095
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0;
}
//将上次时间戳值刷新
lastTimestamp = timestamp;
/**
* 返回结果:
* (timestamp - twepoch) << timestampLeftShift) 表示将时间戳减去初始时间戳,再左移相应位数
* (datacenterId << datacenterIdShift) 表示将数据id左移相应位数
* (workerId << workerIdShift) 表示将工作id左移相应位数
* | 是按位或运算符,例如:x | y,只有当x,y都为0的时候结果才为0,其它情况结果都为1。
* 因为个部分只有相应位上的值有意义,其它位上都是0,所以将各部分的值进行 | 运算就能得到最终拼接好的id
*/
return ((timestamp - twepoch) << timestampLeftShift) |
(datacenterId << datacenterIdShift) |
(workerId << workerIdShift) |
sequence;
}
//获取时间戳,并与上次时间戳比较
private long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
//获取系统时间戳
private long timeGen(){
return System.currentTimeMillis();
}
//---------------测试---------------
public static void main(String[] args) {
IdWorker worker = new IdWorker(1,1,1);
for (int i = 0; i < 30; i++) {
System.out.println(worker.nextId());
}
}
}
优点
- 毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。
- 不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的。
- 可以根据自身业务特性分配bit位,非常灵活。
缺点
- 雪花算法在单机系统上ID是递增的,但是在分布式系统多节点的情况下,所有节点的时钟并不能保证不完全同步,所以有可能会出现不是全局递增的情况。如果系统时间被回调,或者改变,可能会造成id冲突或者重复。
3.利用数据库的auto_increment特性
以MySQL举例,利用给字段设置auto_increment_increment和auto_increment_offset来保证ID自增,每次业务使用下列SQL读写MySQL得到ID号
优点
- 非常简单,利用现有数据库系统的功能实现,成本小,有DBA专业维护。
- ID号单调自增,可以实现一些对ID有特殊要求的业务。
缺点
- 强依赖DB,当DB异常时整个系统不可用,属于致命问题。配置主从复制可以尽可能的增加可用性,但是数据一致性在特殊情况下难以保证。主从切换时的不一致可能会导致重复发号。
- ID发号性能瓶颈限制在单台MySQL的读写性能
- 分表分库,数据迁移合并等比较麻烦
4.Redis的INCR
当使用数据库来生成ID性能不够要求的时候,我们可以尝试使用Redis来生成ID。
这主要依赖于Redis是单线程的,所以也可以用生成全局唯一的ID。可以用Redis的原子操作 INCR和INCRBY来实现。
比较适合使用Redis来生成每天从0开始的流水号。比如订单号=日期+当日自增长号。可以每天在Redis中生成一个Key,使用INCR进行累加。
redis加lua脚本也可以实现twitter的snowflake算法。
优点
-
不依赖于数据库,灵活方便,且性能优于数据库。
-
数字ID天然排序,对分页或者需要排序的结果很有帮助。
缺点
-
如果系统中没有Redis,还需要引入新的组件,增加系统复杂度。
-
需要编码和配置的工作量比较大。
5.参考链接
https://www.jianshu.com/p/da6dae36c290
https://blog.csdn.net/nawenqiang/article/details/82684001
https://segmentfault.com/a/1190000011282426
https://youzhixueyuan.com/how-to-generate-distributed-unique-id.html