开发易忽视的问题:雪花模型实现分析

1,000 阅读4分钟

在 Spring Boot 中使用雪花算法(Snowflake)来生成分布式唯一 ID 是一种常见的实践。雪花算法由 Twitter 提出,由于其高效性和分布式环境中的唯一性,广泛应用于各种场景中。

雪花算法

雪花算法生成的 64 位长整型 ID 一般由以下几部分组成:

  1. 时间戳:通常占用 41 位,用来存储毫秒级的时间戳。
  2. 数据中心 ID:通常占用若干位,用来标识数据中心。
  3. 机器 ID:通常占用若干位,用来标识同一数据中心内的不同机器。
  4. 序列号:通常占用 12 位,用来记录同一毫秒内产生的不同 ID。

实现步骤

  1. 引入依赖

    在你的 pom.xml 文件中添加必要的依赖,例如常用的依赖没有特别指定,可以使用 Spring Boot 的核心依赖。

  2. 编写 Snowflake 算法类

    你可以自己实现一个简单版本的雪花算法,也可以使用已有的库。以下是一个简单的 Java 实现示例:

    public class SnowflakeIdGenerator {
        private final long twepoch = 1288834974657L;
        private final long workerIdBits = 5L;
        private final long datacenterIdBits = 5L;
        private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
        private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
        private final long sequenceBits = 12L;
        private final long workerIdShift = sequenceBits;
        private final long datacenterIdShift = sequenceBits + workerIdBits;
        private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
        private final long sequenceMask = -1L ^ (-1L << sequenceBits);
    
        private long workerId;
        private long datacenterId;
        private long sequence = 0L;
        private long lastTimestamp = -1L;
    
        public SnowflakeIdGenerator(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;
        }
    
        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;
                if (sequence == 0) {
                    timestamp = tilNextMillis(lastTimestamp);
                }
            } else {
                sequence = 0L;
            }
    
            lastTimestamp = timestamp;
    
            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();
        }
    }
    
  3. 使用 Snowflake 算法

    创建一个 Spring Boot 服务,并使用该服务生成唯一 ID:

    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    public class IdController {
    
        private final SnowflakeIdGenerator idGenerator = new SnowflakeIdGenerator(1, 1);
    
        @GetMapping("/generate-id")
        public long generateId() {
            return idGenerator.nextId();
        }
    }
    
  4. 测试

    运行你的 Spring Boot 应用程序,访问 /generate-id 端点,即可获取到一个基于雪花算法生成的唯一 ID。

nextId方法解析

详细解释

  1. 同步关键字(synchronized)

    • 确保在多线程环境下同一时间只有一个线程能执行此方法,防止生成重复的 ID。
  2. 获取当前时间戳

    • long timestamp = timeGen();
    • 调用 timeGen() 方法获取当前的毫秒级时间戳。
  3. 时钟回拨校验

    • if (timestamp < lastTimestamp)
    • 检查当前时间戳是否小于上一次记录的时间戳。如果是,则说明系统时钟出现了回拨,此时抛出异常以避免生成重复 ID。
  4. 同一毫秒内的序列号处理

    • if (lastTimestamp == timestamp)

      • 如果当前时间戳与上次的时间戳相同,则增加序列号以区分在同一毫秒内生成的不同 ID。
      • sequence = (sequence + 1) & sequenceMask; 使用位运算确保序列号不会超过其最大值。
      • if (sequence == 0) 当序列号达到最大值后,等待到下一毫秒再继续生成新的 ID。
  5. 更新最后时间戳

    • lastTimestamp = timestamp;
    • 更新记录的最后时间戳为当前时间戳。
  6. 生成唯一 ID

    • return ((timestamp - twepoch) << timestampLeftShift) | (datacenterId << datacenterIdShift) | (workerId << workerIdShift) | sequence;

    • 将不同字段的信息(时间戳、数据中心 ID、机器 ID、序列号)通过位移和位或运算组合成一个 64 位的长整型 ID。

      • (timestamp - twepoch) << timestampLeftShift) 计算自定义纪元后的时间差,并左移到对应的位置。
      • (datacenterId << datacenterIdShift) 和 (workerId << workerIdShift) 将数据中心 ID 和机器 ID 移到它们各自的位置。
      • | sequence 将序列号放置在最低有效位。

小结

nextId 方法利用时间戳、数据中心 ID、机器 ID 和序列号来生成全局唯一的 ID,通过位移操作将这些元素组合成一个长整型数。该方法确保即使在高并发环境中,生成的 ID 也具有唯一性和顺序性。在实际应用中,这一方法能高效支持分布式系统中的唯一标识需求。

思考题:雪花模型一毫秒内最多生成多少个id

由于序列号占用 12 位,这表示在同一个节点上,同一毫秒可以生成的 ID 数量最多为 (2^{12} = 4096) 个。

因此,如果你的系统有多台机器,每台机器都能在每毫秒生成最多 4096 个唯一 ID,那么总体上你可以通过增加机器数量来扩大每毫秒可以生成的 ID 总量。不过,单台机器在单毫秒内直接生成的 ID 数量上限就是 4096 个。