一、引言
在分布式系统中,生成全局唯一ID是一个常见且关键的需求。传统的自增ID在分布式环境下存在诸多问题:数据库单点瓶颈、ID冲突风险、无法保证顺序性等。
雪花算法(Snowflake Algorithm) 正是为解决这些问题而生的分布式ID生成方案。它由Twitter公司于2010年开源,凭借其高性能、低延迟、可扩展的特性,成为业界最流行的分布式ID生成算法之一。
二、雪花算法的核心思想
雪花算法的核心思想是:将64位整数ID划分为多个部分,每个部分代表不同的含义,通过组合这些部分生成全局唯一的ID。
64位ID结构
0 | 0000000000 0000000000 0000000000 0000000000 0 | 00000 | 00000 | 000000000000
| 部分 | 位数 | 含义 | 作用 |
|---|---|---|---|
| 符号位 | 1位 | 始终为0 | 保证ID为正数 |
| 时间戳 | 41位 | 毫秒级时间戳 | 记录生成时间,保证ID递增 |
| 机器ID | 5位 | 机器标识 | 区分不同机器/节点 |
| 数据中心ID | 5位 | 数据中心标识 | 区分不同数据中心 |
| 序列号 | 12位 | 同一毫秒内的序号 | 同一毫秒内生成多个ID时去重 |
各部分详解
1. 符号位(1位)
- 固定为0,表示正数
- 预留位,未来可能用于扩展
2. 时间戳(41位)
- 使用相对时间戳(相对于某个起始时间)
- 41位可以表示的时间范围:
2^41 - 1毫秒 ≈ 69年 - Twitter的起始时间:
2010-11-04 09:42:54
3. 机器ID(5位)+ 数据中心ID(5位)
- 共10位,可以支持
2^10 = 1024个节点 - 机器ID:区分同一数据中心内的不同机器
- 数据中心ID:区分不同的数据中心
4. 序列号(12位)
- 12位可以表示
2^12 = 4096个数字 - 同一毫秒内,同一节点最多可生成4096个ID
三、算法原理与流程
生成流程
┌─────────────────────────────────────────────────────────────┐
│ 雪花算法ID生成流程 │
├─────────────────────────────────────────────────────────────┤
│ 1. 获取当前毫秒级时间戳 │
│ ↓ │
│ 2. 检查时间戳是否小于上次生成时间(时钟回拨检测) │
│ ↓ │
│ 3. 如果时间戳相同,序列号+1;否则重置序列号为0 │
│ ↓ │
│ 4. 组合各部分:时间戳 << 22 | 数据中心ID << 17 | 机器ID << 12 | 序列号 │
│ ↓ │
│ 5. 返回64位Long型ID │
└─────────────────────────────────────────────────────────────┘
关键设计要点
1. 时间戳处理
// 使用相对时间戳,减少数值大小
private static final long START_TIMESTAMP = 1288834974657L; // Twitter起始时间
long currentTimestamp = System.currentTimeMillis();
long timestamp = currentTimestamp - START_TIMESTAMP;
2. 时钟回拨处理
if (currentTimestamp < lastTimestamp) {
// 时钟回拨,抛出异常或等待
throw new RuntimeException("Clock moved backwards. Refusing to generate id");
}
3. 序列号循环
if (currentTimestamp == lastTimestamp) {
sequence = (sequence + 1) & SEQUENCE_MASK; // 0-4095循环
if (sequence == 0) {
// 序列号用尽,等待下一毫秒
currentTimestamp = waitNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = currentTimestamp;
四、Java实现示例
完整实现
public class SnowflakeIdGenerator {
// 各部分位数
private static final long SEQUENCE_BIT = 12; // 序列号占用位数
private static final long MACHINE_BIT = 5; // 机器标识占用位数
private static final long DATA_CENTER_BIT = 5; // 数据中心占用位数
// 各部分最大值
private static final long MAX_DATA_CENTER_NUM = ~(-1L << DATA_CENTER_BIT);
private static final long MAX_MACHINE_NUM = ~(-1L << MACHINE_BIT);
private static final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BIT);
// 各部分偏移量
private static final long MACHINE_LEFT = SEQUENCE_BIT;
private static final long DATA_CENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
private static final long TIMESTAMP_LEFT = DATA_CENTER_LEFT + DATA_CENTER_BIT;
// Twitter起始时间戳(2010-11-04 09:42:54)
private static final long START_TIMESTAMP = 1288834974657L;
private final long dataCenterId; // 数据中心ID
private final long machineId; // 机器ID
private long sequence = 0L; // 序列号
private long lastTimestamp = -1L; // 上次生成ID的时间戳
public SnowflakeIdGenerator(long dataCenterId, long machineId) {
if (dataCenterId > MAX_DATA_CENTER_NUM || dataCenterId < 0) {
throw new IllegalArgumentException("Data center ID can't be greater than " + MAX_DATA_CENTER_NUM + " or less than 0");
}
if (machineId > MAX_MACHINE_NUM || machineId < 0) {
throw new IllegalArgumentException("Machine ID can't be greater than " + MAX_MACHINE_NUM + " or less than 0");
}
this.dataCenterId = dataCenterId;
this.machineId = machineId;
}
public synchronized long nextId() {
long currentTimestamp = getCurrentTimestamp();
// 时钟回拨检测
if (currentTimestamp < lastTimestamp) {
throw new RuntimeException("Clock moved backwards. Refusing to generate id for " + (lastTimestamp - currentTimestamp) + " milliseconds");
}
// 同一毫秒内,序列号递增
if (currentTimestamp == lastTimestamp) {
sequence = (sequence + 1) & MAX_SEQUENCE;
// 序列号用尽,等待下一毫秒
if (sequence == 0) {
currentTimestamp = waitNextMillis(lastTimestamp);
}
} else {
// 不同毫秒,重置序列号
sequence = 0L;
}
lastTimestamp = currentTimestamp;
// 组合各部分生成ID
return (currentTimestamp - START_TIMESTAMP) << TIMESTAMP_LEFT
| dataCenterId << DATA_CENTER_LEFT
| machineId << MACHINE_LEFT
| sequence;
}
private long getCurrentTimestamp() {
return System.currentTimeMillis();
}
private long waitNextMillis(long lastTimestamp) {
long timestamp = getCurrentTimestamp();
while (timestamp <= lastTimestamp) {
timestamp = getCurrentTimestamp();
}
return timestamp;
}
}
使用示例
public class SnowflakeDemo {
public static void main(String[] args) {
// 创建ID生成器(数据中心ID=1,机器ID=1)
SnowflakeIdGenerator generator = new SnowflakeIdGenerator(1, 1);
// 生成10个ID
for (int i = 0; i < 10; i++) {
long id = generator.nextId();
System.out.println("生成ID: " + id);
System.out.println("二进制: " + Long.toBinaryString(id));
System.out.println();
}
}
}
五、雪花算法的优缺点分析
优点
| 特性 | 说明 |
|---|---|
| 高性能 | 纯内存操作,无需数据库交互,单机可达百万级QPS |
| 低延迟 | 生成ID仅需几纳秒 |
| 全局唯一 | 1024个节点,每个节点每毫秒可生成4096个ID |
| 趋势递增 | 按时间戳递增,利于数据库索引优化 |
| 无中心化 | 分布式部署,无单点故障风险 |
| 可追溯 | ID包含时间戳,可反推生成时间 |
缺点与解决方案
| 问题 | 解决方案 |
|---|---|
| 时钟回拨 | 检测并拒绝生成,或等待时钟同步 |
| 机器ID分配 | 使用配置中心统一管理,或自动检测 |
| 依赖系统时间 | 配置NTP时钟同步服务 |
| 位数限制 | 根据业务需求调整各部分位数 |
六、大厂雪花算法应用对比
| 公司 | 产品/服务 | 核心应用场景 | 优点 | 局限性 |
|---|---|---|---|---|
| 原生雪花算法 | 推文ID、用户ID等核心业务主键 | 经典实现,性能优异,全球海量并发支撑 | 机器ID需手动配置,时钟回拨处理较简单 | |
| 微信 | 分布式ID服务 | 微信生态内各类业务场景 | 高可用设计,支持多数据中心部署 | 依赖内部基础设施,对外不开源 |
| 美团 | Leaf分布式ID系统 | 外卖、到店等核心业务,日均百亿级ID | 支持多种模式(雪花算法/Segment),高性能,稳定性强 | 部署复杂度较高,需要独立运维 |
| 百度 | UidGenerator | 多种业务场景需求 | 支持自定义位数分配,灵活适配业务 | 需要依赖百度内部组件,社区活跃度较低 |
七、MyBatis-Plus集成雪花算法
7.1 MyBatis-Plus中的ID生成策略
MyBatis-Plus提供了多种ID生成策略,通过@TableId注解配置:
@Data
public class BaseEntity<ID extends Serializable> implements Serializable {
@TableId(type = IdType.ASSIGN_ID)
private ID id;
}
7.2 IdType.ASSIGN_ID与雪花算法
IdType.ASSIGN_ID是MyBatis-Plus 3.3.0+版本引入的策略,底层使用雪花算法生成ID:
| IdType | 说明 | 雪花算法支持 |
|---|---|---|
AUTO |
数据库自增 | ❌ |
INPUT |
用户手动输入 | ❌ |
ASSIGN_ID |
雪花算法生成 | ✅ |
ASSIGN_UUID |
UUID生成 | ❌ |
NONE |
无策略 | ❌ |
7.3 时钟回拨检测机制
MyBatis-Plus默认的雪花算法实现会自动检测时钟回拨,处理策略如下:
// MyBatis-Plus内置的时钟回拨处理(简化逻辑)
public class DefaultIdentifierGenerator implements IdentifierGenerator {
private final Snowflake snowflake;
@Override
public Long nextId(Object entity) {
// 内部已包含时钟回拨检测
return snowflake.nextId();
}
}
| 场景 | 处理方式 |
|---|---|
| 轻微回拨(≤5ms) | 等待时钟同步后继续生成 |
| 严重回拨(>5ms) | 抛出IllegalStateException异常 |
7.4 自定义ID生成器
如需自定义时钟回拨处理策略,可实现IdentifierGenerator接口:
@Component
public class CustomIdentifierGenerator implements IdentifierGenerator {
private final SnowflakeIdGenerator snowflakeGenerator;
@Override
public Long nextId(Object entity) {
// 自定义雪花算法实现
return snowflakeGenerator.nextId();
}
}
7.5 使用示例
@Data
@TableName("admin")
public class Admin {
@TableId(type = IdType.ASSIGN_ID)
@Schema(description = "主键ID")
private Long id;
@Schema(description = "用户名")
private String userName;
@Schema(description = "密码")
private String password;
}
八、与其他ID生成方案对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| UUID | 完全去中心化,生成简单 | 无序,不利于索引,字符串存储 |
| 数据库自增 | 简单可靠,有序 | 单点瓶颈,扩展性差 |
| Redis自增 | 高性能,支持分布式 | 依赖Redis,有网络开销 |
| 雪花算法 | 高性能,有序,可扩展 | 依赖时钟,需要机器ID分配 |
九、总结
雪花算法是分布式ID生成领域的经典方案,其设计精巧、性能优异,至今仍被广泛使用。理解其原理不仅有助于正确使用,也能启发我们在设计分布式系统时的思路。
核心要点:
- 64位结构:1位符号位 + 41位时间戳 + 5位数据中心ID + 5位机器ID + 12位序列号
- 关键技术:时间戳差值、序列号循环、时钟回拨检测
- MyBatis-Plus集成:
IdType.ASSIGN_ID自动使用雪花算法,内置时钟回拨检测 - 适用场景:分布式系统、高并发、需要有序ID的场景