我们在设计和实现互联网系统时,往往需要使用到唯一ID。唯一ID标识唯一的一条业务请求,如在电商系统中,ID表示系统中唯一的一个订单,支付系统中表示唯一的一条交易请求。在单机应用中,唯一ID的生成是比较简单的,我们只需保证ID在单机上面是唯一的即可;但目前互联网应用多为微服务应用,同时随着业务的逐渐增长,必须对业务进行分库分表,而且业务应用往往是多实例部署的,这就要求ID在多个微服务应用和多个应用实例之间是唯一。目前业界有很多成熟的唯一ID生成方案,下面我们来看下这些分布式唯一的ID生成方案。
分布式唯一ID, 应该拆开来解释:
此外,根据业务的不同,还可能要存在其他特性:
UUID(universally unique identifier)是一串随机的32位长度的数据,每一位是16进制表示,所以总计能够表示2^128的数字
据统计若每纳秒产生1百万个 UUID,要花100亿年才会将所有 UUID 用完
UUID的生成用到了以太网卡地址、纳秒级时间、芯片ID码和许多可能的数字
UUID为16进制的32字节长度,中间以-
相连,形式为8-4-4-4-12,所以说长度也可以是36,不过使用时一般不包含-
,UUID的形式如下:
ef56e7fd-225b-44b8-b96d-4591bde0945b
********-****-M***-N***-************
上面的以数字
M
开头的四位表示UUID 版本,目前UUID的规范有5个版本,M可选值为1, 2, 3, 4, 5 ;各个版本的具体介绍如下所示:
- version 1:0001。基于时间和 MAC 地址。由于使用了 MAC 地址,因此能够确保唯一性,但是同时也暴露了 MAC 地址,私密性不够好。
- version 2:0010。DCE 安全的 UUID。该版本在规范中并没有仔细说明,因此并没有具体的实现。
- version 3:0011。基于名字空间 (MD5)。用户指定一个名字空间和一个字符串,通过 MD5 散列,生成 UUID。字符串本身需要是唯一的。
- version 4:0100。基于随机数。虽然是基于随机数,但是重复的可能性可以忽略不计,因此该版本也是被经常使用的版本。
- version 5:0101。基于名字空间 (SHA1)。跟 Version 3 类似,但是散列函数编程了 SHA1。
上面以数字
N
开头的四个位表示 UUID 变体( variant ),变体是为了能兼容过去的 UUID,以及应对未来的变化,目前已知的变体有如下几种,因为目前正在使用的 UUID 都是 variant1,所以取值只能是 8,9,a,b 中的一个(分别对应1000,1001,1010,1011)。
- variant 0:0xxx。为了向后兼容预留。
- variant 1:10xx。当前正在使用的。
- variant 2:11xx。为早期微软 GUID 预留。
- variant 3:111x。为将来扩展预留。目前暂未使用。
优点
缺点
Java内置了java.util.UUID
类,其中内置了四种版本的UUID生成策略,包括基于时间和MAC地址的、DCE 安全的UUID、基于名字空间(MD5)和基于随机数的:
There are four different basic types of UUIDs: time-based, DCE
security, name-based, and randomly generated UUIDs. These types have a
version value of 1, 2, 3 and 4, respectively.
比较常用的即是基于随机数的UUID生成,下面是Java的使用方法
UUID.randomUUID().toString();
使用数据库的id自增策略,比如Mysql的auto_increment
优点
缺点
只需设置主键自增即可,如下面的建表语句
CREATE TABLE `user` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id',
`name` varchar(64) NOT NULL COMMENT '名字',
`nick_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '昵称',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
数据库自增模式生成的唯一ID,当遇到分库分表场景时, ID则无法解决ID重复的问题,而且ID依赖于数据库,无法满足业务系统高并发以及高可用的需求.一种解决方案是使用数据库多主模式,也即数据库集群中每个实例都可以生成ID, 并且设置自增ID的起始值和步长
其原理图如下:
优点
缺点
实例1:
set @@auto_increment_offset = 1; -- 起始值
set @@auto_increment_increment = 6; -- 步长
实例2:
set @@auto_increment_offset = 2; -- 起始值
set @@auto_increment_increment = 6; -- 步长
实例3:
set @@auto_increment_offset = 3; -- 起始值
set @@auto_increment_increment = 6; -- 步长
实例4:
set @@auto_increment_offset = 4; -- 起始值
set @@auto_increment_increment = 6; -- 步长
数据和本地缓存相结合的方式, 每个实例首先从数据库中取出一个ID的生成范围,然后需要使用时采用线程安全的方式从本地缓存的ID范围中取出即可,等实例内缓存的ID耗尽时再从数据库中取出一个ID生成范围
优点
缺点
号段模式可以有悲观锁和乐观锁两种实现, 这里我觉得两种方式都可以, 因为该唯一ID的生成方式就说明不会频繁的读写数据库,所以数据库的锁竞争不会很大, 乐观锁和悲观锁方式都可以应该都可以满足需求, 不过使用时还是需要按照具体的业务来设计.
CREATE TABLE id_generator (
id int(10) NOT NULL,
max_id bigint(20) NOT NULL COMMENT '当前最大id',
step int(20) NOT NULL COMMENT '号段的步长',
biz_type int(20) NOT NULL COMMENT '业务类型',
version int(20) NOT NULL COMMENT '版本号',
PRIMARY KEY (`id`)
)
实例取ID的步骤:
CREATE TABLE id_generator (
id int(10) NOT NULL,
max_id bigint(20) NOT NULL COMMENT '当前最大id',
step int(20) NOT NULL COMMENT '号段的步长',
biz_type int(20) NOT NULL COMMENT '业务类型',
version int(20) NOT NULL COMMENT '版本号',
PRIMARY KEY (`id`)
)
实例取ID的步骤:
采用中间件的方式, Redis和Zookeeper都可以实现, 不过Zookeeper我很少使用,这里不再讨论,其原理和Redis方式基本相同
利用redis的 incr命令实现ID的原子性自增。
127.0.0.1:6379> set seq_id 1 // 初始化自增ID为1
OK
127.0.0.1:6379> incr seq_id // 增加1,并返回递增后的数值
(integer) 2
原理和数据库号段模式类似, redis中自增的是号段的起始值, 实例内缓存一个ID范围, 这里不再赘述.
优点
缺点
先设置RedisTemplate:
@Bean
public RedisTemplate getDefaultRedisTemplate(RedisConnectionFactory cf, RedisSerializer> rs) {
RedisTemplate redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(cf);
redisTemplate.setDefaultSerializer(rs);
redisTemplate.setKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
接下来实现ID生成逻辑:
public long generate(String key,int increment) {
RedisAtomicLong counter = new RedisAtomicLong(key, mRedisTemp.getConnectionFactory());
return counter.addAndGet(increment);
}
Twitter公司开源的一种算法, 全局唯一并且趋势递增
雪花算法生成的是8字节64bit长度的数字(long类型), 能够保证趋势递增的ID生成, 而且生成效率极高
雪花算法组成
SnowFlake算法在同一毫秒内最多可以生成的ID数量为: 1024 * 4096 = 4194304
优点
缺点
/**
* 雪花算法
*
* @author Young
* @Date 2021-05-22 17:04
*/
public class SnowflakeIdGenerator {
/**
* 开始时间截 (这个用自己业务系统上线的时间)
*/
private static final long START_TIMESTAMP = 1575365018000L;
/**
* 机器id所占的位数
*/
private static final long WORKER_ID_BITS_LENGTH = 10L;
/**
* 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数)
*/
private static final long MAX_WORKER_ID = -1L ^ (-1L << WORKER_ID_BITS_LENGTH);
/**
* 序列在id中占的位数
*/
private static final long SEQUENCE_BITS_LENGTH = 12L;
/**
* 机器ID向左移12位
*/
private static final long WORKER_ID_SHIFT_LENGTH = SEQUENCE_BITS_LENGTH;
/**
* 时间截向左移22位(10+12)
*/
private static final long TIMESTAMP_LEFT_SHIFT_LENGTH = SEQUENCE_BITS_LENGTH + WORKER_ID_BITS_LENGTH;
/**
* 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095)
*/
private static final long SEQUENCE_MASK = -1L ^ (-1L << SEQUENCE_BITS_LENGTH);
/**
* 工作机器ID(0~1024)
*/
private long workerId;
/**
* 毫秒内序列(0~4095)
*/
private long sequence = 0L;
/**
* 上次生成ID的时间截
*/
private long lastTimestamp = -1L;
/**
* 构造函数
*
* @param workerId 工作ID (0~1024)
*/
public SnowflakeIdGenerator(long workerId) {
if (workerId > MAX_WORKER_ID || workerId < 0) {
throw new IllegalArgumentException(String.format("workerId can't be greater than %d or less than 0", MAX_WORKER_ID));
}
this.workerId = workerId;
}
// ==============================Methods==========================================
/**
* 获得下一个ID (该方法是线程安全的)
*
* @return SnowflakeId
*/
public synchronized long nextId() {
long timestamp = currentTimestampMillis();
// 时钟回退处理, 时钟回退一般是10ms内
if (timestamp < lastTimestamp) {
// 如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过, 这个时候取上次时间戳作为当前时间
timestamp = lastTimestamp;
}
if (lastTimestamp == timestamp) {
// 如果是同一时间生成的,则进行毫秒内序列
sequence = (sequence + 1) & SEQUENCE_MASK;
// 毫秒内序列溢出
if (sequence == 0) {
// 阻塞到下一个毫秒,获得新的时间戳
timestamp = nextTimestampMillis(lastTimestamp);
}
} else {
//时间戳改变,毫秒内序列重置
sequence = 0L;
}
// 更新上次生成ID的时间截
lastTimestamp = timestamp;
//移位并通过或运算拼到一起组成64位的ID
return ((timestamp - START_TIMESTAMP) << TIMESTAMP_LEFT_SHIFT_LENGTH)
| (workerId << WORKER_ID_SHIFT_LENGTH)
| sequence;
}
/**
* 阻塞到下一个毫秒,直到获得新的时间戳
*
* @param lastTimestamp 上次生成ID的时间截
* @return 当前时间戳
*/
private long nextTimestampMillis(long lastTimestamp) {
long timestamp = currentTimestampMillis();
while (timestamp <= lastTimestamp) {
timestamp = currentTimestampMillis();
}
return timestamp;
}
/**
* 返回以毫秒为单位的当前时间
*
* @return 当前时间(毫秒)
*/
private long currentTimestampMillis() {
return System.currentTimeMillis();
}
}
推荐原系统的设计和开发者写的Leaf——美团点评分布式ID生成系统, 已经非常详细了, 小的就不再班门弄斧了
另外这篇也值得一看 Leaf:美团的分布式唯一ID方案深入剖析
美团Leaf算法有两种模式:
百度uid-generator也是类snowflake算法, 其主要特点有:
TinyId
滴滴开源的Tinyid如何每天生成亿级别的ID?
TinyId的主要特点有: