1.为什么要生成唯一ID?
数据唯一性:每个记录都需要有一个独一无二的标识符来确保数据的唯一性。这可以避免重复的数据行,并有助于准确地查询、更新或删除特定的记录。
数据完整性:通过使用唯一ID,可以保证数据库中的数据完整性。例如,在关系数据库中,外键通常引用主表的唯一ID,以建立和维护表之间的关联。
高效查询:唯一ID通常是索引的一部分,这意味着使用它们进行查询可以非常快速和高效。这对于提高应用程序性能特别重要,尤其是在处理大量数据时。
简化逻辑处理:拥有一个唯一的标识符可以简化很多业务逻辑的处理。例如,当需要引用或共享特定数据项时,唯一ID提供了一个简单而直接的方法。
分布式系统支持:在分布式系统或微服务架构中,唯一ID(特别是全局唯一ID)是必不可少的。它们允许不同的服务能够独立生成不冲突的ID,从而简化了跨服务的数据整合和通信问题。
易于扩展和维护:随着系统的发展和需求的变化,唯一ID使得添加新功能或者修改现有功能变得更加容易。例如,重构数据库结构时,唯一ID可以帮助更平滑地迁移数据。
2.生成唯一ID的基本要求
唯一性
:这是最基本的要求,即生成的ID在整个系统中必须是独一无二的。特别是在分布式系统中,需要确保不同节点生成的ID也不会发生冲突。
全局唯一性(如果适用):在某些场景下,如分布式系统或微服务架构中,可能需要ID不仅在同一数据库或系统内唯一,而且在所有相关的系统和数据库中也保持唯一。
不可预测性
:为了安全考虑,尤其是在涉及用户敏感信息或重要业务逻辑时,ID应该是难以预测的。这可以防止恶意猜测或其他安全问题。
顺序性(如果需要):有些应用可能需要ID有一定的顺序性,例如按时间顺序排列,以便于排序、分页等操作。但需要注意的是,严格的顺序性可能会对系统的扩展性和性能造成影响。
高效性:生成ID的过程应该尽可能快速且消耗资源少,以避免成为系统性能瓶颈。
长度适中
:ID的长度应该适中,既能保证足够的空间来确保唯一性,又不至于过长导致存储和传输效率降低。
兼容性:生成的ID应与现有的系统和协议兼容,不会因为格式或编码问题而引起错误。
可扩展性
:随着系统的发展和规模的增长,生成ID的方法应能方便地进行扩展,以适应更高的需求。
3.常见生成方法
- UUID(通用唯一识别码)
基于时间的UUID(Version 1):结合时间戳和MAC地址生成。
基于随机数的UUID(Version 4):使用随机数生成。
基于命名空间的UUID(Version 3 和 5):基于命名空间和名称的哈希值生成。
优点:
全局唯一性
:UUID的设计保证了在全球范围内的唯一性,适用于分布式系统。
无需中央协调
:无需依赖数据库或其他中央服务生成ID,减少了系统复杂性。
生成速度快
:基于随机数的UUID生成速度非常快,适用于高并发场景。缺点:
长度较长
:UUID长度为128位,通常表示为36个字符(包括连字符),这在存储和传输时占用较多空间。例如,在数据库中存储UUID会比存储整数类型占用更多的空间。
无序性
:UUID通常是随机生成的,缺乏时间上的顺序性。这在数据库索引中可能导致性能问题,因为插入操作可能需要在B树索引中频繁分裂节点。
可读性差
:UUID对人类不友好,难以记忆和识别,不利于在用户界面或日志中直接使用。
缺乏业务相关性
:UUID不包含任何业务信息,如时间戳或区域信息,难以用于业务分析和追踪。
- 适用场景
- 分布式系统:在多节点、多数据中心的环境中,UUID是生成唯一标识符的理想选择。
- 无需排序的场景:如果不需要按时间或其他顺序对ID进行排序,UUID是一个不错的选择。
- 高并发环境:UUID的生成速度非常快,适用于需要快速生成唯一标识符的高并发场景。
数据库自增ID是一种常见的生成唯一标识符的方法,通过在数据库表中设置自增字段(如
AUTO_INCREMENT
),每次插入新记录时,数据库会自动为该字段生成一个唯一的、递增的整数。
优点:
简单易用:实现简单,只需在数据库表中设置自增字段,无需额外的代码或配置。
有序性:自增ID是有序的,有利于数据库索引的性能,特别是在使用B树索引时。
节省存储空间:整数类型的ID通常比UUID占用更少的存储空间。
可读性较好:整数ID对人类相对友好,易于识别和记忆。
缺点:
单点瓶颈:在分布式数据库环境中,自增ID难以保证全局唯一性,通常需要依赖单个数据库实例来生成ID,这会成为系统的单点瓶颈,影响性能和可扩展性。
难以水平扩展:如果系统需要水平扩展到多个数据库实例,自增ID的生成会成为问题,因为每个实例都会生成自己的ID序列,导致ID冲突。
依赖数据库:生成ID依赖于数据库,这在数据库故障或高负载时可能成为问题。
缺乏业务相关性:自增ID不包含任何业务信息,如时间戳或区域信息,难以用于业务分析和追踪。
- 适用场景
- 单体应用:在单体应用中,数据库自增ID是一个简单且有效的方法。
- 无需分布式唯一性的场景:如果系统不需要在多个数据库实例或多个数据中心中保持全局唯一性,自增ID是一个不错的选择。
- 对ID有序性有要求的场景:如果需要按时间或其他顺序对ID进行排序,自增ID的有序性是一个优势。
4.雪花算法的实现过程如下
获取当前时间戳,精确到毫秒级别。
根据给定的数据中心ID和机器ID,生成一个10位的二进制数。
将时间戳左移22位,将数据中心ID左移17位,将机器ID左移12位,然后使用位或操作符将它们组合成一个64位的二进制数。
如果在同一毫秒内生成了多个ID,使用序列号来区分它们,序列号从0开始递增,最多可以生成4096个序列号。
优点
:全局唯一性:在分布式系统中,雪花算法可以确保生成的ID全局唯一。
有序性:生成的ID按照时间戳有序递增,便于数据管理和查询。
高并发:每毫秒可以生成4096个ID,适合高并发场景。
缺点
:依赖服务器时间:如果服务器时间回拨,可能会导致生成重复的ID。可以通过记录最后一个生成ID的时间戳来解决这个问题。
序列号浪费:在分库分表时,如果序列号一直从0开始,可能会导致数据倾斜和不均匀分布。
`适用场景
:分布式系统:如分布式数据库、分布式锁等,需要全局唯一且有序的ID。
高并发场景:如订单号生成、用户ID生成等
- 实现代码
public class SnowflakeIdGenerator {
// 开始时间戳 (2023-01-01)
private final long twepoch = 1672502400000L;
// 机器ID所占的位数
private final long workerIdBits = 5L;
// 数据标识ID所占的位数
private final long datacenterIdBits = 5L;
// 支持的最大机器ID,结果是31 (这个值在位运算中不会溢出)
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
// 支持的最大数据标识ID,结果是31
private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
// 序列号在ID中所占的位数
private final long sequenceBits = 12L;
// 机器ID需要左移的位数,12
private final long workerIdShift = sequenceBits;
// 数据标识ID需要左移的位数,17
private final long datacenterIdShift = sequenceBits + workerIdBits;
// 时间戳需要左移的位数,22
private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
// 生成序列的掩码,这里为4095 (0b111111111111=4095)
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
// 工作机器ID(0~31)
private long workerId;
// 数据中心ID(0~31)
private long datacenterId;
// 毫秒内序列(0~4095)
private long sequence = 0L;
// 上次生成ID的时间戳
private long lastTimestamp = -1L;
public SnowflakeIdGenerator(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;
}
/**
* 产生下一个ID
*/
public synchronized long nextId() {
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) & sequenceMask;
// 毫秒内序列溢出
if (sequence == 0) {
// 阻塞到下一毫秒,获得新的时间戳
timestamp = tilNextMillis(lastTimestamp);
}
} else {
// 时间戳改变,毫秒内序列重置
sequence = 0L;
}
// 上次生成ID的时间截
lastTimestamp = timestamp;
// 移位并通过或运算拼到一起组成64位的ID
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();
}
}