Snowflake 雪花算法优缺点(Java老司机实战总结)
✍ 作者:天天摸鱼的java工程师
💼 从业8年,主攻Java后端 + 分布式架构
📌 今天我们来聊聊:Snowflake 雪花算法
💡 背景:为什么需要雪花算法?
我们在做订单系统、用户系统、支付系统时,经常需要全局唯一ID(非数据库主键) :
- 分布式环境下不能用数据库自增ID;
- UUID 虽然唯一,但不有序 、不易读、占用空间大;
- 雪花算法就是为了解决这些问题而生的。
🌨 什么是 Snowflake?
Snowflake(雪花算法) 最早由 Twitter 提出,用于在分布式系统中生成全局唯一的、有序的64位整数ID。
🧬 雪花ID结构(64位)
位数 | 含义 | 示例解释 |
---|---|---|
1位 | 符号位 | 永远为0 |
41位 | 时间戳 | 当前时间戳 - 起始时间戳(单位毫秒) |
10位 | 机器ID | 包括数据中心ID + 机器ID,最多支持1024台 |
12位 | 序列号 | 每毫秒内支持4096个ID |
🧮 雪花ID样例
ini
1010100110010110000000000000000000000110000010010000000000010010
=>
178972842389893122
✅ Snowflake 优点(为啥大家都爱它)
1️⃣ 高性能
- 纯内存计算,生成一个ID只需微秒级时间;
- 每毫秒能生成4096个ID,适合高并发场景。
2️⃣ 全局唯一 & 有序
- 基于时间戳生成,天然有序;
- 多节点部署也不会重复,保证唯一性。
3️⃣ 不依赖数据库
- 不用数据库锁,不用中心协调;
- 分布式部署非常友好,支持横向扩展。
4️⃣ 数字ID,可读性好
- 相比UUID,更短更可读;
- 适合用于订单号、用户ID、日志追踪等业务。
❌ Snowflake 缺点(老司机踩过的坑)
1️⃣ 时间依赖强 → 时钟回拨问题
-
如果服务器时间回拨(如NTP校时),可能生成重复ID;
-
解决方案:
- 检测回拨并抛异常;
- 等待回拨时间过去;
- 使用全局时钟服务(如 TSO)。
2️⃣ 单点故障风险(如果只部署一个节点)
- 单节点部署虽然简单,但失效就挂了;
- 建议部署多台机器,配合机器ID 或 数据中心ID 使用。
3️⃣ ID 不可读,不支持趋势分析
- 虽然是数字,但不直观;
- 不能看出 ID 属于哪个业务、用户等 → 可用业务前缀解决
4️⃣ 序列号耗尽(每毫秒最多4096个)
-
极端高并发场景下,可能溢出;
-
解决方式:
- 等待下一毫秒;
- 增加机器节点分流请求。
🔧 Java 实现 Snowflake 算法(实战代码)
ini
public class SnowflakeIdGenerator {
// 起始时间戳(2020-01-01)
private final long epoch = 1577808000000L;
private final long datacenterIdBits = 5L;
private final long workerIdBits = 5L;
private final long sequenceBits = 12L;
private final long maxWorkerId = ~(-1L << workerIdBits); // 31
private final long maxDatacenterId = ~(-1L << datacenterIdBits); // 31
private final long workerIdShift = sequenceBits; // 12
private final long datacenterIdShift = sequenceBits + workerIdBits; // 17
private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits; // 22
private final long sequenceMask = ~(-1L << sequenceBits); // 4095
private long lastTimestamp = -1L;
private long sequence = 0L;
private final long workerId;
private final long datacenterId;
public SnowflakeIdGenerator(long workerId, long datacenterId) {
if (workerId > maxWorkerId || datacenterId > maxDatacenterId) {
throw new IllegalArgumentException("workerId or datacenterId out of range");
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
public synchronized long nextId() {
long timestamp = currentTime();
if (timestamp < lastTimestamp) {
throw new RuntimeException("Clock moved backwards. Refusing to generate id");
}
if (timestamp == lastTimestamp) {
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
// 当前毫秒内序列号用完,等待下一个毫秒
timestamp = waitNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp - epoch) << timestampLeftShift)
| (datacenterId << datacenterIdShift)
| (workerId << workerIdShift)
| sequence;
}
private long waitNextMillis(long lastTimestamp) {
long timestamp = currentTime();
while (timestamp <= lastTimestamp) {
timestamp = currentTime();
}
return timestamp;
}
private long currentTime() {
return System.currentTimeMillis();
}
}
🧪 测试效果
arduino
public class Main {
public static void main(String[] args) {
SnowflakeIdGenerator generator = new SnowflakeIdGenerator(1, 1);
for (int i = 0; i < 5; i++) {
System.out.println(generator.nextId());
}
}
}
erlang
142345234354123456
142345234354123457
...
🚀 适用场景
业务场景 | 是否推荐 | 原因说明 |
---|---|---|
分布式订单系统 | ✅ 推荐 | 唯一 + 有序 + 无需中心协调 |
用户ID生成 | ✅ 推荐 | 可读性好,唯一性强 |
微服务请求追踪ID | ✅ 推荐 | 全局唯一,便于日志追踪 |
高并发日志系统 | ✅ 推荐 | 性能高且有序 |
数据库主键 | ✅ 推荐 | 替代自增ID,支持水平分表 |
🧩 延伸阅读
- ↑ 你可以结合 Redis、ZooKeeper、Etcd 等方式来分配机器ID;
- ↑ 也可以替换时间戳逻辑为 逻辑时钟 / TSO(如TiDB的实现);
✅ 总结
优点 | 缺点 |
---|---|
高性能 | 依赖时间,易受回拨影响 |
全局唯一 & 有序 | 不支持趋势分析 |
分布式友好 | 单机部署易成单点 |
数字ID 可读性强 | 每毫秒最多生成 4096 个ID |
🗣️ 最后
如果你觉得这篇文章对你有帮助,欢迎:
👉 点赞 👍 + 收藏 ⭐ + 关注 🧡
👉 在评论区聊聊你们的分布式ID生成方案