一、UUID:我即是唯一
UUID 是 通用唯一识别码(Universally Unique Identifier)的缩写,是一种软件建构的标准,亦为开放软件基金会组织在分布式计算环境领域的一部分。其目的,是让分布式系统中的所有元素,都能有唯一的辨识信息,而不需要通过中央控制端来做辨识信息的指定。如此一来,每个人都可以创建不与其它人冲突的UUID。在这样的情况下,就不需考虑数据库创建时的名称重复问题。目前最广泛应用的UUID,是微软公司的全局唯一标识符(GUID),而其他重要的应用,则有Linux ext2/ext3文件系统、LUKS加密分区、GNOME、KDE、Mac OS X等等。另外我们也可以在e2fsprogs包中的UUID库找到实现。–来源自百度百科
UUID说白了就是在大量数据中用来表示唯一一个实体的唯一标识,就像身份证号码,其实雪花算法我们扒开算法实现看内容的话它的生成策略也和我们的身份证有着异曲同工之妙,这个我们一会儿再看。
通常我们会在以下场景用到UUID:
- 用户表中某个用户的ID
- 文件存储服务中某个文件的唯一编号
- 商城或者转帐中某个订单的单号
UUID生成可以依赖的元素:
网卡MAC地址、时间戳、名字空间(Namespace)、随机或伪随机数、时序等元素
当然除了这些在业务中的用法,现在的UUID和数据库自增主键通常被用作为分布式ID,但是在Mysql数据库中,因为通常需要事务支持,会使用Innodb存储引擎,UUID太长以及无序,所以并不适合在Innodb中来作为主键,自增主键ID比较合适,随着公司的业务发展,数据量将越来越大,需要对数据进行分表,而分表后,每个表中的数据都会按自己的节奏进行自增,很有可能出现ID冲突。这时就需要一个单独的机制来负责生成唯一ID。所以来自twitter开源的分布式ID生成算法–雪花算法就成为了一种行之有效的分布式ID生成方式(除此之外还有基于数据库的号段模式和基于Redis自增方式)。
二、SnowFlake算法:想做唯一的那片雪花:
上面说过SnowFlake算法和身份证有异曲同工之妙,下面我们来看看SnowFlake算法的核心思想:
生成的分布式ID固定是一个long型的数字,一个long型占8个字节,也就是64个bit,原始snowflake算法中对于bit的分配如下图:
光看这个结构是不是觉得和身份证很像,同样都是分位来代表不同的信息,来唯一确定一个ID;
再来看一下分段的具体含义:
- 符号位部分:1位,在java中由于long的最高位是符号位固定为0;
- 时间戳部分:41bit,这个是毫秒级的时间,一般实现上不会存储当前的时间戳,而是时间戳的差值(当前时间-固定的开始时间),这样可以使产生的ID从更小值开始;41位的时间戳可以使用69年,(1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69年;
- 工作机器id:10bit,这里比较灵活,比如,可以使用前5位作为数据中心机房标识,后5位作为单机房机器标识,可以部署1024个节点;
- 序列号部分:12bit,主要用于并发访问,支持同一毫秒内同一个节点可以生成4096个ID;
同样用身份证来类比:
- 籍贯地址编号—>工作机器ID
- 出生年月日—>时间戳部分
- 最后的四位—>序列号
这样看下来是不是很像,这样去理解雪花算法的生成策略就觉得并没有什么了不起的,下面是雪花算法的Java实现:
public class IdWorker{
private long workerId;
private long datacenterId;
private long sequence = 0;
/**
* 固定的时间戳,用于计算时间戳差值部分
*/
private long twepoch = 1611072299L;
private long workerIdBits = 5L;
private long datacenterIdBits = 5L;
private long sequenceBits = 12L;
private long workerIdShift = sequenceBits;
private long datacenterIdShift = sequenceBits + workerIdBits;
private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
// 得到初始UUID,0000000000000000000000000000000000000000000000000000111111111111
private long sequenceMask = -1L ^ (-1L << sequenceBits);
private long lastTimestamp = -1L;
public IdWorker(long workerId, long datacenterId){
this.workerId = workerId;
this.datacenterId = datacenterId;
}
public synchronized long nextId() {
long timestamp = timeGen();
//时间回拨,抛出异常处理
//通常来说如果时间回拨时间短,比如配置5ms以内,那么可以直接等待一定的时间,让机器的时间追上来。
//还可以利用扩展位来直接赋值
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));
}
// 并发访问的控制
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0;
}
lastTimestamp = timestamp;
return ((timestamp - twepoch) << timestampLeftShift) |
(datacenterId << datacenterIdShift) |
(workerId << workerIdShift) |
sequence;
}
/**
* 当前ms已经满了
*/
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);
for (int i = 0; i < 50; i++) {
System.out.println(worker.nextId());
}
}
}
三、实际应用中的雪花算法:适合自己的才是最好的:
百度(uid-generator)
uid-generator使用的就是snowflake,主要差别在于workId的生成。
uid-generator中的workId是由uid-generator自动生成的,并且考虑到了应用部署在docker上的情况,在uid-generator中用户可以自己去定义workId的生成策略,默认提供的策略是:应用启动时由数据库分配(由host,port组成)。
除了workid之外,时间戳也有所不同,uid-generator用的是秒来作为时间戳部分而不是毫秒。
美团(Leaf)
美团的Leaf也是一个分布式ID生成框架。支持号段模式,也支持snowflake模式。
Leaf中的snowflake模式和原始snowflake算法的不同点,主要在workId的生成,Leaf中workId是基于ZooKeeper的顺序Id来生成的,每个应用在使用Leaf-snowflake时,在启动时都会都在Zookeeper中生成一个顺序Id,相当于一台机器对应一个顺序节点,也就是一个workId。