问题

雪花算法是什么?

答案

1. 核心概念

雪花算法(Snowflake) 是Twitter开源的分布式ID生成算法,通过组合时间戳、机器标识和序列号生成64位长整型的全局唯一ID。该算法能够在分布式环境下高效生成趋势递增的唯一ID,广泛应用于订单号、用户ID等场景。

核心特点

  • 生成的ID是64位Long型整数
  • 趋势递增,对数据库索引友好
  • 不依赖数据库等第三方系统,性能极高
  • 每秒可生成百万级ID(理论可达400万+/秒/节点)

2. 算法原理及结构

2.1 ID结构组成(64位)

┌─────────┬──────────────────────────────────┬──────────┬──────────┬──────────────┐
│  1 bit  │           41 bits                │  5 bits  │  5 bits  │   12 bits    │
│  符号位 │         时间戳(毫秒)            │ 机房ID   │ 机器ID   │   序列号     │
│    0    │    (最多可用69年)                │  (0-31)  │  (0-31)  │  (0-4095)    │
└─────────┴──────────────────────────────────┴──────────┴──────────┴──────────────┘

各部分详解

  1. 符号位(1位):始终为0,保证生成的ID为正数
  2. 时间戳(41位)
    • 存储相对于起始时间的毫秒数差值
    • 起始时间(epoch)可自定义,如 1288834974657L(2010-11-04)
    • 41位可表示约 2^41 / 1000 / 60 / 60 / 24 / 365 ≈ 69年
  3. 数据中心ID(5位):支持32个数据中心(0-31)
  4. 机器ID(5位):每个数据中心支持32台机器(0-31)
  5. 序列号(12位):同一毫秒内的递增序列,支持4096个ID(0-4095)

2.2 生成能力计算

  • 单机器单毫秒:最多生成 2^12 = 4096 个ID
  • 单机器每秒:理论上可生成 4096 × 1000 = 409.6万个ID
  • 集群容量:最多支持 32 × 32 = 1024 台机器

3. 完整Java实现

public class SnowflakeIdWorker {

    // ==============================常量配置==============================
    /** 起始时间戳 (2015-01-01) */
    private final long twepoch = 1420041600000L;

    /** 机器ID所占的位数 */
    private final long workerIdBits = 5L;

    /** 数据中心ID所占的位数 */
    private final long datacenterIdBits = 5L;

    /** 序列号所占的位数 */
    private final long sequenceBits = 12L;

    /** 机器ID最大值:31 */
    private final long maxWorkerId = -1L ^ (-1L << workerIdBits);

    /** 数据中心ID最大值:31 */
    private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);

    /** 序列号掩码:4095 */
    private final long sequenceMask = -1L ^ (-1L << sequenceBits);

    /** 机器ID左移位数:12 */
    private final long workerIdShift = sequenceBits;

    /** 数据中心ID左移位数:17 */
    private final long datacenterIdShift = sequenceBits + workerIdBits;

    /** 时间戳左移位数:22 */
    private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

    // ==============================实例变量==============================
    /** 工作机器ID (0~31) */
    private long workerId;

    /** 数据中心ID (0~31) */
    private long datacenterId;

    /** 毫秒内序列 (0~4095) */
    private long sequence = 0L;

    /** 上次生成ID的时间戳 */
    private long lastTimestamp = -1L;

    // ==============================构造函数==============================
    /**
     * @param workerId     工作ID (0~31)
     * @param datacenterId 数据中心ID (0~31)
     */
    public SnowflakeIdWorker(long workerId, long datacenterId) {
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(String.format(
                "worker Id can't be greater than %d or less than 0", maxWorkerId));
        }
        if (datacenterId > maxDatacenterId || datacenterId < 0) {
            throw new IllegalArgumentException(String.format(
                "datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
        }
        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }

    // ==============================核心方法==============================
    /**
     * 获得下一个ID (线程安全)
     */
    public synchronized long nextId() {
        long timestamp = timeGen();

        // 时钟回拨检测
        if (timestamp < lastTimestamp) {
            throw new RuntimeException(String.format(
                "Clock moved backwards. Refusing to generate id for %d milliseconds",
                lastTimestamp - timestamp));
        }

        // 同一毫秒内
        if (lastTimestamp == timestamp) {
            // 序列号递增
            sequence = (sequence + 1) & sequenceMask;

            // 毫秒内序列溢出(超过4095)
            if (sequence == 0) {
                // 阻塞到下一毫秒
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            // 新的毫秒,序列号重置为0
            sequence = 0L;
        }

        // 更新上次生成ID的时间戳
        lastTimestamp = timestamp;

        // 组装64位ID
        return ((timestamp - twepoch) << timestampLeftShift)
                | (datacenterId << datacenterIdShift)
                | (workerId << workerIdShift)
                | sequence;
    }

    /**
     * 阻塞到下一个毫秒,直到获得新的时间戳
     */
    protected long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    /**
     * 返回当前时间(毫秒)
     */
    protected long timeGen() {
        return System.currentTimeMillis();
    }

    // ==============================ID解析方法==============================
    /**
     * 解析ID的各个组成部分
     */
    public static String parseId(long id) {
        long timestamp = (id >> 22) + 1420041600000L;
        long datacenterId = (id >> 17) & 0x1F;
        long workerId = (id >> 12) & 0x1F;
        long sequence = id & 0xFFF;

        return String.format(
            "ID: %d\nTimestamp: %d (%s)\nDatacenterId: %d\nWorkerId: %d\nSequence: %d",
            id, timestamp, new Date(timestamp), datacenterId, workerId, sequence);
    }
}

4. 关键问题与解决方案

4.1 时钟回拨问题

问题描述:服务器时间被NTP同步或人为修改导致时钟回拨,可能产生重复ID。

解决方案

方案1:拒绝服务(上述代码实现)

if (timestamp < lastTimestamp) {
    throw new RuntimeException("Clock moved backwards");
}

方案2:等待追上

if (timestamp < lastTimestamp) {
    long offset = lastTimestamp - timestamp;
    if (offset <= 5) {
        // 小幅回拨(5ms内),等待追上
        Thread.sleep(offset << 1);
        timestamp = timeGen();
    } else {
        throw new RuntimeException("Clock moved backwards");
    }
}

方案3:使用备用workerId

// 时钟回拨时切换到备用机器ID,继续服务
if (timestamp < lastTimestamp) {
    workerId = (workerId + 1) % maxWorkerId;
}

方案4:百度UidGenerator方案

  • 使用RingBuffer预生成ID
  • 时钟回拨时从缓存中获取
  • 业界较成熟的生产级解决方案

4.2 机器ID分配问题

分配方式

  1. 配置文件方式
    snowflake:
      datacenter-id: 1
      worker-id: 3
    
  2. IP地址取模
    public static long getWorkerIdByIp() {
     try {
         String ip = InetAddress.getLocalHost().getHostAddress();
         String[] parts = ip.split("\\.");
         return Long.parseLong(parts[3]) % 32;
     } catch (Exception e) {
         return new Random().nextInt(32);
     }
    }
    
  3. ZooKeeper自动分配
    // 创建临时有序节点,节点序号作为workerId
    String path = zkClient.create(
     "/snowflake/worker-",
     null,
     ZooDefs.Ids.OPEN_ACL_UNSAFE,
     CreateMode.EPHEMERAL_SEQUENTIAL
    );
    long workerId = Long.parseLong(path.substring(path.lastIndexOf("-") + 1)) % 32;
    
  4. 数据库分配
    // 服务启动时从数据库获取未使用的workerId并标记为已使用
    

4.3 并发安全问题

问题:多线程环境下可能生成重复ID

解决方案

  1. synchronized关键字(如上述实现)
  2. Lock锁机制: ```java private final ReentrantLock lock = new ReentrantLock();

public long nextId() { lock.lock(); try { // ID生成逻辑 } finally { lock.unlock(); } }


3. **ThreadLocal + 多实例**:
```java
private static final ThreadLocal<SnowflakeIdWorker> WORKER_THREAD_LOCAL =
    ThreadLocal.withInitial(() -> new SnowflakeIdWorker(workerId++, datacenterId));

5. 优化改进版本

5.1 美团Leaf-Snowflake

改进点

  • 使用ZooKeeper管理workerId,避免手动配置
  • 解决时钟回拨:缓存workerId,回拨时使用新的workerId
  • 弱依赖ZooKeeper:本地缓存workerId

5.2 百度UidGenerator

特点

  • RingBuffer预生成:异步生成ID并缓存
  • 时间回拨容忍:使用缓存的ID
  • 秒级精度:时间戳改为秒级,扩展序列号位数

结构调整

1位符号 + 28位秒级时间戳 + 22位WorkerId + 13位序列号

6. 生产环境应用建议

使用场景

  • ✅ 订单ID、交易流水号(需要趋势递增)
  • ✅ 用户ID、商品ID(需要高并发生成)
  • ✅ 消息ID、事件ID(需要全局唯一)
  • ❌ 不适合对ID连续性有严格要求的场景

部署建议

  1. 独立服务部署:提供HTTP/RPC接口统一生成ID
  2. 嵌入应用内:直接集成到业务代码中(更高性能)
  3. 高可用保障:多实例部署 + 负载均衡
  4. 监控告警:监控时钟回拨、ID生成失败等异常

性能优化

// 批量生成ID,减少锁竞争
public List<Long> nextIds(int count) {
    List<Long> ids = new ArrayList<>(count);
    synchronized (this) {
        for (int i = 0; i < count; i++) {
            ids.add(nextId());
        }
    }
    return ids;
}

7. 面试答题总结

简洁版回答

雪花算法是Twitter开源的分布式ID生成算法,生成64位Long型ID。结构为:1位符号位 + 41位时间戳 + 10位机器ID(5位数据中心+5位机器) + 12位序列号。优点是本地生成、性能高、趋势递增;主要问题是时钟回拨可能导致ID重复,需要通过拒绝服务、等待追上或切换workerId等方式解决。单机每秒可生成400万+ID,适合订单号、用户ID等高并发场景。

进阶要点

  • 理解64位结构的每一部分含义和作用
  • 掌握时钟回拨问题的多种解决方案
  • 了解机器ID的分配策略(手动配置、ZooKeeper、IP取模等)
  • 熟悉美团Leaf、百度UidGenerator等工业级实现
  • 能够根据业务特点调整算法参数(如秒级时间戳、调整序列号位数)