分布式ID生成器的常见方案与雪花算法(Snowflake)详解
字数 1776 2025-12-15 17:05:52
分布式ID生成器的常见方案与雪花算法(Snowflake)详解
分布式ID生成器是在分布式系统中生成全局唯一ID的工具,用于替代单机数据库的自增ID,以满足高并发、高可用的需求。
1. 需求与挑战
在分布式系统中,生成唯一ID面临以下挑战:
- 全局唯一性: 不同节点生成的ID不能重复。
- 趋势递增: 通常希望ID具有时间或数字上的递增趋势,便于数据库索引(如InnoDB的聚簇索引)。
- 高性能: 生成ID的速度要快,延迟要低。
- 高可用: 生成器服务要具备高可用性,避免单点故障。
- 信息安全: ID中不应包含可能泄露业务信息的敏感内容(如订单号中直接包含用户ID)。
2. 常见方案概览
-
UUID:
- 标准格式为36位字符串,基于MAC地址、时间戳、随机数等生成。
- 优点:本地生成,性能极高,全球唯一。
- 缺点: 字符串存储空间大,查询效率低;无序,作为主键时插入效率差。
-
数据库自增ID:
- 利用数据库的自增主键功能,通过
replace into或事务获取ID。 - 优点: 实现简单,ID绝对有序。
- 缺点: 数据库是单点,有性能瓶颈;分库分表时难以保证全局唯一。
- 利用数据库的自增主键功能,通过
-
数据库号段模式:
- 不是每次生成都访问数据库,而是从数据库预先获取一个号段(如1~1000),缓存在本地,用完后再次获取。
- 优点: 大大降低了数据库访问频次,性能好。
- 缺点: 需要引入额外的服务来管理号段。
-
基于Redis:
- 利用Redis的
INCR或INCRBY命令生成自增序列。 - 优点: 性能优于数据库。
- 缺点: 需维护Redis集群,存在持久化、数据丢失等问题。
- 利用Redis的
-
雪花算法(Snowflake):
- Twitter开源的分布式ID生成算法,是目前工业界最流行的方案之一。
3. 雪花算法(Snowflake)核心原理
雪花算法生成的是一个64位的长整型数字ID,其二进制结构如下:
0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000
- 1位符号位: 固定为0,表示正数。
- 41位时间戳: 记录当前时间戳与一个自定义起始时间戳(epoch)的差值(毫秒)。
- 10位工作机器ID: 高5位是数据中心ID(datacenterId),低5位是工作节点ID(workerId)。最多支持
2^10 = 1024个节点。 - 12位序列号: 同一毫秒内产生的不同ID的序列号。支持每毫秒生成
2^12 = 4096个ID。
生成过程:
- 获取当前时间戳(毫秒),计算与起始时间的差值,填入41位。
- 判断当前时间是否与上一次生成ID的时间相同。
- 如果相同,则将序列号加1。如果序列号溢出(超过4095),则等待至下一毫秒。
- 如果不同,则将序列号重置为0。
- 结合机器ID,拼接成一个64位的长整型数字。
4. 代码实现与关键点
public class SnowflakeIdGenerator {
// 起始时间戳 (2020-01-01 00:00:00)
private final long epoch = 1577808000000L;
// 机器ID所占位数
private final long workerIdBits = 5L;
private final long datacenterIdBits = 5L;
// 序列号所占位数
private final long sequenceBits = 12L;
// 最大值计算
private final long maxWorkerId = -1L ^ (-1L << workerIdBits); // 31
private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); // 31
private final long maxSequence = -1L ^ (-1L << sequenceBits); // 4095
// 移位偏移量
private final long workerIdShift = sequenceBits; // 12
private final long datacenterIdShift = sequenceBits + workerIdBits; // 17
private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits; // 22
private long workerId;
private long datacenterId;
private long sequence = 0L;
private long lastTimestamp = -1L;
public SnowflakeIdGenerator(long workerId, long datacenterId) {
// 参数检查
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException("workerId 超出范围");
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException("datacenterId 超出范围");
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
public synchronized long nextId() {
long timestamp = timeGen();
// 时钟回拨处理
if (timestamp < lastTimestamp) {
throw new RuntimeException("时钟回拨,拒绝生成ID");
}
if (lastTimestamp == timestamp) {
// 同一毫秒内,序列号递增
sequence = (sequence + 1) & maxSequence;
if (sequence == 0) {
// 当前毫秒序列号用完,等待下一毫秒
timestamp = tilNextMillis(lastTimestamp);
}
} else {
// 不同毫秒,序列号重置
sequence = 0L;
}
lastTimestamp = timestamp;
// 拼接并返回ID
return ((timestamp - epoch) << timestampLeftShift)
| (datacenterId << datacenterIdShift)
| (workerId << workerIdShift)
| sequence;
}
private long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
private long timeGen() {
return System.currentTimeMillis();
}
}
5. 关键问题与优化
- 时钟回拨: 服务器时间可能因同步而回调,导致ID重复。上面代码进行了简单检测并抛出异常。更健壮的做法可以是:
- 短暂回拨(如几毫秒): 等待时钟追回。
- 长时间回拨: 报警人工介入,或扩展工作机器ID的位数,借用未来时间。
- 工作机器ID分配: 如何保证每个节点的
workerId和datacenterId全局唯一?- 可以通过ZooKeeper、Etcd等协调服务分配。
- 或通过数据库、配置文件、启动参数等方式静态配置。
- 性能: 本地内存计算,无网络开销,性能极高(每秒可生成数十万ID)。
- 趋势递增: 由于高位是时间戳,所以整体是趋势递增的,对数据库索引友好。
6. 变体与改进方案
雪花算法是此类“时间戳+机器标识+序列号”方案的典范,但业界有很多变体:
- 美团Leaf: 结合了号段模式和雪花算法。号段模式用于对递增要求不高的业务,雪花模式用于对趋势递增和性能要求高的业务,并优化了时钟回拨问题。
- 百度UidGenerator: 基于雪花算法,但将时间单位从毫秒改为秒,并将部分时间戳位与序列号位结合,支持更长的服务时间。
- 滴滴TinyID: 基于数据库号段模式,提供HTTP服务。
总结
雪花算法以其简单、高效、ID趋势递增的优点,成为分布式ID生成的主流方案。在实际应用中,需要重点解决机器ID分配和时钟回拨两大问题,并可以根据业务场景(如并发量、ID长度要求、是否需要绝对有序)选择最合适的变体或组合方案。