在分布式系统中,生成全局唯一ID是个经典问题。今天咱们就来聊聊两种常见的解决方案:雪花算法和优化版UUID。这两种方案各有特点,适用于不同场景,咱们慢慢道来。
一、为什么需要分布式ID
在单机系统中,用数据库自增ID就能满足需求。但在分布式环境下,多个节点同时生成ID,简单的自增就会出问题。比如订单系统有多个实例,每个实例都从1开始自增,那肯定会重复。
分布式ID需要满足几个基本要求:
- 全局唯一,不能重复
- 趋势递增,有利于数据库索引
- 高可用,生成速度快
- 尽量短,节省存储空间
二、雪花算法原理与实现
雪花算法是Twitter开源的分布式ID生成算法,结构如下:
- 1位符号位,固定为0
- 41位时间戳,精确到毫秒,可用69年
- 10位工作机器ID,支持1024个节点
- 12位序列号,每毫秒可生成4096个ID
来看个Java实现示例:
public class SnowflakeIdGenerator {
// 起始时间戳(2020-01-01)
private final static long START_TIMESTAMP = 1577808000000L;
// 机器ID占位数
private final static long WORKER_ID_BITS = 5L;
// 数据中心ID占位数
private final static long DATACENTER_ID_BITS = 5L;
// 序列号占位数
private final static long SEQUENCE_BITS = 12L;
// 最大机器ID
private final static long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);
// 最大数据中心ID
private final static long MAX_DATACENTER_ID = ~(-1L << DATACENTER_ID_BITS);
private long workerId; // 机器ID
private long datacenterId; // 数据中心ID
private long sequence = 0L; // 序列号
private long lastTimestamp = -1L; // 上次生成ID的时间戳
public SnowflakeIdGenerator(long workerId, long datacenterId) {
// 参数校验
if (workerId > MAX_WORKER_ID || workerId < 0) {
throw new IllegalArgumentException("worker Id 超出范围");
}
if (datacenterId > MAX_DATACENTER_ID || datacenterId < 0) {
throw new IllegalArgumentException("datacenter Id 超出范围");
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
public synchronized long nextId() {
long timestamp = System.currentTimeMillis();
// 时钟回拨处理
if (timestamp < lastTimestamp) {
throw new RuntimeException("时钟回拨,拒绝生成ID");
}
// 同一毫秒内生成多个ID
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & ((1 << SEQUENCE_BITS) - 1);
if (sequence == 0) { // 当前毫秒序列号用完,等待下一毫秒
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L; // 新毫秒重置序列号
}
lastTimestamp = timestamp;
// 拼接各部分生成最终ID
return ((timestamp - START_TIMESTAMP) << (WORKER_ID_BITS + DATACENTER_ID_BITS + SEQUENCE_BITS))
| (datacenterId << (WORKER_ID_BITS + SEQUENCE_BITS))
| (workerId << SEQUENCE_BITS)
| sequence;
}
// 等待下一毫秒
private long tilNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
}
使用示例:
public class Main {
public static void main(String[] args) {
// 创建ID生成器(workerId=1, datacenterId=1)
SnowflakeIdGenerator idGenerator = new SnowflakeIdGenerator(1, 1);
// 生成10个ID
for (int i = 0; i < 10; i++) {
long id = idGenerator.nextId();
System.out.println("生成的ID: " + id);
}
}
}
三、UUID的优化方案
标准UUID有36个字符,太长且无序。我们可以优化它:
- 去掉连字符,长度从36降到32
- 使用更紧凑的Base64编码,长度降到22
- 加入时间戳前缀,使其有序
Java优化实现:
import java.nio.ByteBuffer;
import java.util.Base64;
import java.util.UUID;
public class OptimizedUUID {
// 使用Base64编码(URL安全,去掉填充字符)
private static final Base64.Encoder BASE64_ENCODER =
Base64.getUrlEncoder().withoutPadding();
// 生成优化版UUID
public static String generate() {
// 1. 生成标准UUID
UUID uuid = UUID.randomUUID();
// 2. 转换为字节数组
byte[] bytes = new byte[16];
ByteBuffer buffer = ByteBuffer.wrap(bytes);
buffer.putLong(uuid.getMostSignificantBits());
buffer.putLong(uuid.getLeastSignificantBits());
// 3. Base64编码
return BASE64_ENCODER.encodeToString(bytes);
}
// 带时间戳前缀的优化版
public static String generateWithTimestamp() {
long timestamp = System.currentTimeMillis();
String uuid = generate();
return timestamp + "-" + uuid;
}
}
使用示例:
public class Main {
public static void main(String[] args) {
// 生成普通优化版UUID
String id1 = OptimizedUUID.generate();
System.out.println("优化版UUID: " + id1);
// 生成带时间戳的优化版UUID
String id2 = OptimizedUUID.generateWithTimestamp();
System.out.println("带时间戳的优化版UUID: " + id2);
}
}
四、两种方案的对比与应用场景
雪花算法适用场景:
- 需要有序ID的场合,如数据库主键
- 对存储空间敏感的系统
- 需要知道ID生成时间的系统
优化版UUID适用场景:
- 不需要顺序性的ID
- 分布式系统中不依赖中心时钟
- 需要极简配置的场景
性能对比:
- 雪花算法依赖系统时钟,时钟回拨会出问题
- UUID不依赖任何外部状态,但无序影响数据库性能
- 雪花算法生成的ID更短,节省存储空间
五、注意事项与最佳实践
- 雪花算法要注意时钟回拨问题,可以记录上次生成ID的时间戳,发现回拨就报警
- 工作机器ID要确保不重复,可以用ZooKeeper或数据库分配
- UUID优化版虽然解决了长度问题,但仍然无序,不适合做数据库主键
- 在高并发场景下,雪花算法的序列号可能不够用,可以适当减少工作机器ID位数,增加序列号位数
六、总结
雪花算法和优化版UUID各有千秋。如果需要有序、紧凑的ID,雪花算法是首选。如果追求简单、不需要顺序性,优化版UUID更合适。实际项目中,可以根据业务特点灵活选择,甚至组合使用两种方案。
评论