雪花算法原理

雪花算法(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毫秒级时间戳
机器 ID10工作节点 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. 相对时间戳

原理: 使用相对时间戳,节省位数。


实现:


机器 ID 设计

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. 需要严格递增

❌ 需要严格递增
❌ 趋势递增不满足

总结

雪花算法评价

维度评分说明
唯一性⭐⭐⭐⭐⭐理论保证
有序性⭐⭐⭐⭐⭐趋势递增
性能⭐⭐⭐⭐⭐高性能
可用性⭐⭐⭐⭐本地生成
扩展性⭐⭐⭐⭐分布式友好

使用建议

强烈推荐:

✅ 分布式系统
✅ 大规模系统
✅ 高性能要求
✅ 需要有序性

下一步

了解了雪花算法原理后,我们学习时钟回拨问题及解决方案。

👉 下一节:时钟回拨

发现内容有问题?

告诉我们哪里需要改进。