UUID
UUID(Universally Unique Identifier,通用唯一识别码)是一种软件构建的标准,用于生成唯一的标识符。
UUID旨在保证在空间和时间上的唯一性,即任意两个UUID在所有地方和所有时间都不相同。
UUID的格式
UUID的标准表示形式为32个16进制数字,分为五组,组之间用连字符(-)分隔,格式为8-4-4-4-12
,例如:123e4567-e89b-12d3-a456-426614174000
。
这五部分的含义如下:
- 第1组:时间戳的低32位加上固定的4位(版本号,对于UUID v1来说是1)
- 第2组:时间戳中接下来的16位,加上固定的2位(版本号,对于UUID v1来说是1)和2位(时钟序列的最高两位)
- 第3组:版本1的UUID中这一部分由IEEE分配给厂商的全球唯一标识符(通常是MAC地址),以及其他版本的UUID有不同的含义
- 第4组:这一部分包含一个随机或伪随机的值,加上固定的2位(时钟序列的剩余6位)
- 第5组:这一部分是前面三部分的校验码
UUID的版本
UUID有多个版本,每个版本有不同的生成方法:
- UUID v1(基于时间):基于时间的UUID结合了机器的MAC地址和当前时间戳,保证了空间和时间的唯一性。
- UUID v2(DCE安全):DCE(分布式计算环境)安全的UUID与v1类似,但加入了本地域组件。
- UUID v3(基于名字):基于名字的UUID通过散列(MD5)一个名字空间和一个名字来生成。
- UUID v4(随机):随机UUID使用随机数或伪随机数来生成,有很低的概率产生重复。
- UUID v5(基于名字):与v3类似,但使用SHA-1散列算法。
生成UUID的方法有很多,以下是几种常见的生成方式:
- 伪随机数生成: 对于UUID v4,可以使用伪随机数生成器来生成UUID。
- 基于时间: 对于UUID v1,结合当前时间和机器的MAC地址来生成。
- 基于名字: 对于UUID v3和v5,使用散列算法将名字空间和一个特定的名字散列成一个UUID。
由于UUID v1和v2包含机器的MAC地址,这可能会引发隐私问题。为了解决这个问题,可以使用UUID v4或v5,它们不包含任何与机器或时间相关的信息。此外,UUID v3和v5使用散列函数,可以提供额外的安全性。
UUID的优缺点
优点:
- 全球唯一性: 几乎可以保证在全球范围内生成的UUID是唯一的。
- 无需中心化: 生成UUID不需要中心化的管理或注册过程。
缺点:
- 空间占用: UUID比许多其他标识符类型占用更多的存储空间。
- 不可读性: UUID对于人类来说没有直观意义,不便于记忆和识别。
- 不适合范围查询:由于UUID不是自增的,因此不支持范围查询,这可能会影响查询效率。
- 性能: 生成UUID可能需要更多的计算资源,尤其是在需要大量生成时。
数据库ID
基于自增
数据库自增ID依赖于数据库系统提供的自增功能和相应的锁机制。当向数据库中插入一条新记录时,不需要指定ID,数据库会自动为该记录分配一个唯一的、递增的ID。
优点
- 简单易用:由数据库自动管理ID的生成,无需复杂的业务逻辑。
- 顺序递增:生成的ID是顺序递增的,便于排序和索引。
缺点
- 单点故障:如果数据库服务器出现问题,整个系统可能无法生成新的ID。
- 性能瓶颈:在高并发的场景下,频繁地写入操作可能会导致数据库的压力增大,影响性能。
在分布式系统中,数据库自增ID作为分布式ID生成算法会导致扩展性差的问题:
- ID重复:当系统进行水平扩展,使用多个数据库实例时,每个实例都有自己的自增序列,可能会导致ID重复。
- ID不连续:即使解决了ID重复问题,不同数据库实例生成的ID之间也可能不连续。
基于号段模式
号段模式的核心思想是将数据库的自增ID机制从每次生成一个ID变为每次生成一批ID,并将这批ID缓存在业务服务器上。业务服务器在用完这批ID后,再向数据库请求新的号段。
通过预分配一段ID号段来减少对数据库的访问频率,从而提高ID生成的性能。
实现步骤
- 数据库准备: 创建一个数据库表来存储ID的当前最大值和步长(号段大小)。sql
CREATE TABLE `id_generator` ( `id` INT NOT NULL AUTO_INCREMENT, `max_id` BIGINT NOT NULL DEFAULT 1, `step` INT NOT NULL DEFAULT 1000, `biz_type` VARCHAR(20) NOT NULL DEFAULT '', PRIMARY KEY (`id`), UNIQUE KEY `uk_biz_type` (`biz_type`) );
- 初始化号段: 在业务启动时,从数据库中加载一个号段,并缓存在业务服务器内存中。sql
SELECT max_id, step FROM id_generator WHERE biz_type = 'ORDER' FOR UPDATE;
- 生成ID: 在业务服务器上,每次需要生成ID时,从缓存的号段中取一个ID,并递增内部计数器。java
long currentId = maxId + stepCounter; stepCounter++;
- 更新号段: 当业务服务器用完当前号段后,需要重新从数据库加载新的号段。sql
UPDATE id_generator SET max_id = max_id + step WHERE biz_type = 'ORDER'; SELECT max_id, step FROM id_generator WHERE biz_type = 'ORDER' FOR UPDATE;
优点
- 减少数据库访问:通过预分配号段,减少了每次生成ID时对数据库的访问,提高了性能。
- 扩展性好:在分布式系统中,多个业务服务器可以并行获取不同的号段,互不影响。
- 容错性:即使某个业务服务器发生故障,其他服务器仍然可以继续生成ID。
缺点
- ID不连续:由于不同业务服务器可能会同时更新号段,因此生成的ID在全局范围内可能不是完全连续的。
- 内存占用:需要将号段缓存在业务服务器的内存中,可能会占用一定的内存资源。
- 号段竞争:在分布式环境下,多个业务服务器可能会同时请求更新号段,需要通过数据库锁(如乐观锁或悲观锁)来避免竞争条件。
- 时钟同步:虽然号段模式不直接依赖于时间戳,但如果业务需要ID中包含时间信息,则仍然需要考虑时钟同步问题。
Redis生成ID
Redis生成ID主要利用了Redis的两个原子操作命令:INCR
和 INCRBY
。INCR
命令用于将键的整数值增加1,如果键不存在,则初始化为0再增加1。INCRBY
命令可以增加指定的整数。
实现步骤
- 选择一个Redis键:为生成ID的操作选择一个专用的Redis键,例如
global:id
。 - 生成ID:
- 使用
INCR
命令生成单个ID。bashINCR global:id
- 使用
INCRBY
命令生成多个ID,例如一次性生成10个ID。bashINCRBY global:id 10
- 使用
- 获取ID:执行上述命令后,Redis会返回一个新的ID值。
简单实现
ID的组成部分:符号位:1bit,永远为0
时间戳:31bit,以秒为单位,可以使用69年
序列号:32bit,秒内的计数器,支持每秒产生2^32个不同ID
查看代码
@Component
public class RedisIdWorker {
/**
* 开始时间戳
*/
private static final long BEGIN_TIMESTAMP = 1711023321L;
/**
* 序列号的位数
*/
private static final int COUNT_BITS = 32;
private StringRedisTemplate stringRedisTemplate;
public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
public long nextId(String keyPrefix) {
// 1.生成时间戳
LocalDateTime now = LocalDateTime.now();
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long timestamp = nowSecond - BEGIN_TIMESTAMP;
// 2.生成序列号
// 2.1.获取当前日期,精确到天
String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
// 2.2.自增长
long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
// 3.拼接并返回
return timestamp << COUNT_BITS | count;
}
}
优点
- 高性能:Redis是基于内存的键值存储,读写速度快,适合生成ID这种高频操作。
- 原子性:
INCR
和INCRBY
命令是原子操作,即使在并发环境下也能保证ID的唯一性。 - 灵活性:可以根据需要生成单个或多个ID,而且可以通过设置不同的键来生成不同用途的ID。
缺点
- 依赖Redis:如果Redis是单机部署,那么存在单点故障的风险,将无法生成新的ID。
- ID长度限制:虽然Redis的整数类型可以存储非常大的数,但在实际应用中,生成的ID长度通常受限于业务需求。
- 持久化问题:Redis的数据存储在内存中,如果发生故障重启,需要确保ID的持久化,避免ID的重复。
雪花算法
雪花算法(Snowflake Algorithm)是一种分布式系统中生成唯一ID的算法,它最早由Twitter提出并使用。雪花算法能够保证在分布式系统中,不同机器在同一个毫秒内生成的ID是唯一的。它生成的ID是一个64位的整数,结构如下:
0 - 41位时间戳(毫秒级) - 5位数据中心ID - 5位机器ID - 12位序列号
组成
- 时间戳(Timestamp):占41位,精确到毫秒,可以使用69年。时间戳是相对于一个特定时间的偏移量,这个特定时间通常是系统开始使用雪花算法的时间。
- 优点:由于时间戳占据了最大的位数,因此可以保证随着时间的推移,生成的ID是递增的,这有助于在数据库等存储系统中保持较高的插入性能。
- 数据中心ID(Datacenter ID):占5位,最多可以表示32个数据中心。
- 作用:根据不同的业务需求,可以将服务部署在不同的数据中心,这个ID可以用来标识这个ID是由哪个数据中心生成的。
- 机器ID(Worker ID):占5位,每个数据中心可以部署最多32台机器。
- 作用:在同一数据中心内,不同的机器或者实例可以用这个ID来区分。
- 序列号(Sequence Number):占12位,用来记录同一毫秒内生成的不同ID,这个序列号在同一毫秒内从0开始,每次生成ID时加1,直到达到4095,然后等待下一个毫秒。
- 作用:保证了同一毫秒内生成的ID不会冲突。
生成过程
- 获取时间戳:首先获取当前时间戳,如果当前时间戳小于上一次生成ID的时间戳,说明系统时钟回拨了,这时算法需要等待直到时钟追赶到上一次的时间戳。
- 计算数据中心ID和机器ID:根据配置或者环境变量获取。
- 生成序列号:如果当前时间戳与上一次的时间戳相同,则序列号加1,否则序列号重置为0。
- 拼接:将时间戳、数据中心ID、机器ID和序列号拼接成一个64位的整数。
优点
- 高性能:生成速度快,能够满足高并发场景。
- 唯一性:几乎可以保证全球范围内的唯一性。
- 有序性:生成的ID按时间戳递增,便于排序和索引。
当然,雪花算法也有它的局限性,比如它依赖于系统时钟的准确性,如果系统时钟发生回拨,可能会导致ID生成问题。此外,它生成的ID长度是固定的64位,这在某些特定场景下可能不是最优的选择。
简单实现
查看代码
public class SnowflakeIdWorker {
// 系统开始时间戳(自定义,这里以1288834974657为例)
private final long twepoch = 1288834974657L;
// 数据中心ID所占的位数
private final long datacenterIdBits = 5L;
// 机器ID所占的位数
private final long workerIdBits = 5L;
// 序列号所占的位数
private final long sequenceBits = 12L;
// 数据中心ID最大值
private final long maxDatacenterId = ~(-1L << datacenterIdBits);
// 机器ID最大值
private final long maxWorkerId = ~(-1L << workerIdBits);
// 序列号最大值
private final long sequenceMask = ~(-1L << sequenceBits);
// 数据中心ID左移位数
private final long datacenterIdShift = sequenceBits + workerIdBits;
// 机器ID左移位数
private final long workerIdShift = sequenceBits;
// 时间戳左移位数
private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
// 数据中心ID
private long datacenterId;
// 机器ID
private long workerId;
// 序列号
private long sequence = 0L;
// 上次生成ID的时间戳
private long lastTimestamp = -1L;
public SnowflakeIdWorker(long workerId, long datacenterId) {
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));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
public synchronized long nextId() {
long timestamp = timeGen();
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) & sequenceMask;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp - twepoch) << timestampLeftShift) |
(datacenterId << datacenterIdShift) |
(workerId << workerIdShift) |
sequence;
}
protected long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
protected long timeGen() {
return System.currentTimeMillis();
}
public static void main(String[] args) {
SnowflakeIdWorker idWorker = new SnowflakeIdWorker(1, 1);
for (int i = 0; i < 10; i++) {
long id = idWorker.nextId();
System.out.println(id);
}
}
}