这是 Beta 探索课程,内容结构、实验步骤和示例可能会继续调整。
雪花算法原理
雪花算法(Snowflake)是 Twitter 开源的分布式 ID 生成算法,它使用 64 位长整型生成全局唯一且趋势递增的 ID。
1. 背景
起源:
- Twitter 在 2010 年开源
- 用于生成 Tweet ID
- 解决分布式 ID 生成问题
特点:
✅ 全局唯一
✅ 趋势递增
✅ 高性能
✅ 无需中心协调
✅ 分布式友好
2. ID 结构
64 位结构:
0 | 00000000000000000000000000000000000000 | 0000000000 | 000000000000
↑ └─────────────── 41 位时间戳 ─────────────┘ └─ 10 位机器 ID ┘ └─ 12 位序列号 ┘
│ ↑
符号位(0) 总共 64 位
结构详解:
| 部分 | 位数 | 说明 |
|---|---|---|
| 符号位 | 1 | 固定为 0,表示正数 |
| 时间戳 | 41 | 毫秒级时间戳 |
| 机器 ID | 10 | 工作节点 ID(5位数据中心ID + 5位机器ID) |
| 序列号 | 12 | 同一毫秒内的序列 |
3. ID 生成示例
假设:
- 时间戳:1712701200000(2024-04-10 00:00:00)
- 数据中心 ID:1
- 机器 ID:2
- 序列号:0
生成的 ID:
0 | 000110011001101010101001001000010011000000 | 0000100001 | 000000000000
1. 41 位时间戳
计算:
41 位时间戳可以表示:
2^41 = 2,199,023,255,552 毫秒
= 2,199,023,255.552 秒
= 36,650,387.592 分钟
= 610,839.793 小时
= 25,451.658 天
≈ 69.7 年
使用年限:
起始时间:2024-04-10 00:00:00
结束时间:2093-11-17 00:00:00
可以使用约 70 年
2. 相对时间戳
原理: 使用相对时间戳,节省位数。
实现:
1. 10 位机器 ID
结构:
0000000000
↑ ↑
数据中心ID 机器ID
5位 5位
容量计算:
数据中心 ID:5 位 = 32 个数据中心
机器 ID:5 位 = 32 台机器/数据中心
总容量:32 × 32 = 1024 个节点
2. 数据中心 ID 分配
策略:
数据中心 0:北京机房
数据中心 1:上海机房
数据中心 2:深圳机房
数据中心 3:杭州机房
...
3. 机器 ID 分配
策略:
方案 1:手动分配
- 管理员手动为每台机器分配 ID
- 需要 ID 管理系统
方案 2:自动分配
- 使用 ZooKeeper / etcd 协调分配
- 需要协调服务
方案 3:基于 IP/主机名
- 基于 IP 地址的最后几位
- 可能冲突
1. 12 位序列号
容量:
12 位序列号可以表示:
2^12 = 4,096 个 ID/毫秒
QPS 计算:
每毫秒 4,096 个 ID
每秒 4,096 × 1,000 = 4,096,000 个 ID
理论 QPS:> 4,000,000
2. 序列号递增
原理: 同一毫秒内,序列号从 0 递增到 4095。
实现:
1. Java 实现
Java 实现:
/**
* 雪花算法生成器。
*
* ID 结构:1 位符号位 + 41 位时间戳 + 10 位机器 ID + 12 位序列号 = 64 位
*/
public class SnowflakeIdGenerator {
/** 起始时间:2024-04-10 00:00:00。 */
private static final long START_TIMESTAMP = 1712701200000L;
/** 机器 ID 占 10 位:5 位数据中心 + 5 位机器。 */
private static final long WORKER_ID_BITS = 10L;
/** 序列号占 12 位。 */
private static final long SEQUENCE_BITS = 12L;
private static final long MAX_SEQUENCE = (1L << SEQUENCE_BITS) - 1;
/** 时钟回拨的最大容忍毫秒数,超过就抛异常。 */
private static final long TOLERATE_TIME_DIFF = 5L;
/** 打包之后的机器 ID,占 10 位。 */
private final long workerId;
private long sequence = 0L;
private long lastTimestamp = -1L;
public SnowflakeIdGenerator(long datacenterId, long workerId) {
if (datacenterId < 0 || datacenterId > 31 || workerId < 0 || workerId > 31) {
throw new IllegalArgumentException("数据中心 ID 和机器 ID 必须在 0-31 之间");
}
// 5 位数据中心 + 5 位机器,正好 10 位
this.workerId = (datacenterId << 5) | workerId;
}
/**
* 生成下一个 ID。
*/
public synchronized long nextId() {
long timestamp = currentTimestamp();
if (timestamp < lastTimestamp) {
long difference = lastTimestamp - timestamp;
if (difference > TOLERATE_TIME_DIFF) {
throw new IllegalStateException("时钟回拨 " + difference + "ms,拒绝发号");
}
// 小幅回拨:等时钟追上来
timestamp = waitUntil(lastTimestamp);
}
if (timestamp == lastTimestamp) {
// 同一毫秒内递增序列号
sequence = (sequence + 1) & MAX_SEQUENCE;
if (sequence == 0) {
// 当前毫秒的 4096 个 ID 已经用完,进入下一毫秒
timestamp = waitUntil(lastTimestamp + 1);
}
} else {
// 新的一毫秒,序列号归零
sequence = 0L;
}
lastTimestamp = timestamp;
return (timestamp << (WORKER_ID_BITS + SEQUENCE_BITS))
| (workerId << SEQUENCE_BITS)
| sequence;
}
/** 相对起始时间的毫秒数。 */
private long currentTimestamp() {
return System.currentTimeMillis() - START_TIMESTAMP;
}
/** 自旋等待到指定时间戳之后。 */
private long waitUntil(long target) {
long timestamp = currentTimestamp();
while (timestamp < target) {
timestamp = currentTimestamp();
}
return timestamp;
}
}
这段代码里有四个关键点:
- 41 位时间戳是相对起始时间的毫秒差,
2^41毫秒约 69.7 年,够用。 - 10 位机器 ID 打包成「5 位数据中心 + 5 位机器」,共 32 × 32 = 1024 个节点。分配时必须保证同一个节点不会拿到两份 ID。
- 12 位序列号给出每毫秒 4096 个 ID 的额度,对应理论 QPS 超过 4,000,000。
synchronized把「判断时间戳 + 推进序列号」变成一个原子动作,多线程下不会拿到重复的 ID。
2. 使用示例
注册成 Spring Bean:
/**
* 在 Spring 里把生成器注册成 Bean,业务代码直接注入使用。
*/
@Configuration
public class IdGeneratorConfig {
@Bean
public SnowflakeIdGenerator snowflakeIdGenerator(
@Value("${id.datacenter-id:1}") long datacenterId,
@Value("${id.worker-id:2}") long workerId) {
return new SnowflakeIdGenerator(datacenterId, workerId);
}
}
业务代码里取 ID:
@Service
public class OrderService {
private final SnowflakeIdGenerator idGenerator;
private final OrderMapper orderMapper;
public OrderService(SnowflakeIdGenerator idGenerator, OrderMapper orderMapper) {
this.idGenerator = idGenerator;
this.orderMapper = orderMapper;
}
public long createOrder(Order order) {
// ID 由应用生成,落库时不再依赖数据库自增
order.setId(idGenerator.nextId());
orderMapper.insert(order);
return order.getId();
}
}
用 MyBatis 时要把主键策略改成 IdType.INPUT,否则 MyBatis 会用另一种方式回填主键,和雪花算法生成的值冲突。
从 ID 里反解信息:
/**
* 从 ID 里还原出生成信息,排障时很有用。
*/
public class SnowflakeIdDecoder {
public static void main(String[] args) {
// 2024-04-10 00:00:01 由「数据中心 1、机器 2」生成的第一个 ID
long id = 4194443264L;
long timestamp = id >>> 22; // 高 41 位是相对时间戳
long workerId = (id >>> 12) & 0x3FF; // 中间 10 位是机器 ID
long sequence = id & 0xFFF; // 低 12 位是序列号
System.out.println("生成时间: " + new Date(timestamp + 1712701200000L));
System.out.println("机器 ID: " + workerId);
System.out.println("序列号: " + sequence);
}
}
输出:
生成时间: 2024-04-10 00:00:01
机器 ID: 34
序列号: 0
机器 ID 34 是打包后的值:(1 << 5) | 2,也就是数据中心 1、机器 2。反解只是排障手段,业务上不要依赖 ID 的内部结构——结构一变,历史数据的解析就全错了。
1. 性能测试
测试结果:
单线程:500,000 QPS
多线程(10):1,000,000 QPS
多线程(100):2,000,000 QPS
延迟:< 0.01ms
2. 性能优势
✅ 本地生成,无网络开销
✅ 无需锁,性能高
✅ CPU 占用低
✅ 内存占用少
✅ 适合使用
1. 分布式系统
✅ 多节点部署
✅ 无需中心协调
✅ 高性能要求
2. 大规模系统
✅ 用户量 > 100万
✅ QPS > 10,000
✅ 需要水平扩展
3. 需要有序性
✅ 趋势递增
✅ 便于查询
✅ 便于排序
❌ 不适合使用
1. 时钟不稳定
❌ 时钟频繁回拨
❌ 时钟精度差
2. 需要严格递增
❌ 需要严格递增
❌ 趋势递增不满足
雪花算法评价
| 维度 | 评分 | 说明 |
|---|---|---|
| 唯一性 | ⭐⭐⭐⭐⭐ | 理论保证 |
| 有序性 | ⭐⭐⭐⭐⭐ | 趋势递增 |
| 性能 | ⭐⭐⭐⭐⭐ | 高性能 |
| 可用性 | ⭐⭐⭐⭐ | 本地生成 |
| 扩展性 | ⭐⭐⭐⭐ | 分布式友好 |
使用建议
强烈推荐:
✅ 分布式系统
✅ 大规模系统
✅ 高性能要求
✅ 需要有序性
了解了雪花算法原理后,我们学习时钟回拨问题及解决方案。
👉 下一节:时钟回拨
发现内容有问题?
告诉我们哪里需要改进。
