雪花算法

分布式 ID 生成方案。

雪花算法的核心思想是将一个64位的ID分成多个部分,每个部分代表不同的含义。

2023-05-04_16-02-04

具体分为四个部分:

  • 第一部分,占用 1 个 bit,其值永远是 0 ,因为 ID 永远都是正数 0 表示正数。
  • 第二部分是时间戳,占用 41 个 bit ,单位为毫秒,最大可以容纳约 69 年的时间,这里的时间戳不会从 1970 年开始计算,我们可以其从任意时间开始计算。
  • 第三部分是工作机器的 ID ,占用 10 个 bit,高五位为数据中心 ID,低五位为工作节点 ID 最多可以容纳 1024 个节点。
  • 第四部分为序列号,占用 12 个 bit,用来记录同毫秒内产生的不同 ID 。每个节点每毫秒从 0 开始累加,最多可以累加到 4095,同一毫秒最多可以产生 4096 个 ID。

实现逻辑如下:

  1. 首先实例化我们的节点对象,实例化参数分别为集群中统一使用的时间和节点的 ID 值。
  2. 因为存在并发的情况,所以生成 ID 的操作必须使用锁。
  3. 生成 ID 时,比对当前时间和记录的时间是否一致,如果一致则序列号+1。如果序列号满了则等待下一毫秒。如果不一致则序列号保持为 0。
  4. 记录新的时间戳。
  5. 拼接三段二进制的数据,使用位移和或运算拼接。
  6. 返回生成的 ID。

使用 Java 语言简单实现该算法。

package com.example.demo.snowflake;

public class Snowflake {

    // 基准时间戳
    private long baseTimestamp = 0L;
    // 上次记录的时间戳偏移量
    private long lastTimestamp = 0L;
    // 工作机器ID
    private long workerID = 0L;
    // 序列号
    private long sequence = 0L;

    // 序列号最大值
    private static final long MAX_SEQUENCE = 4096L;
    // 序列号位数
    private static final long SEQUENCE_BITS = 12L;
    // 工作机器ID位数
    private static final long WORKER_ID_BITS = 10L;
    // 工作机器ID最大值
    private static final long MAX_WORKER_ID = 1024L;
    // 工作机器ID左移位数
    private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;
    // 时间戳左移位数
    private static final long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;

    public Snowflake(long baseTimestamp, long workerID) {
        if (System.currentTimeMillis() < baseTimestamp) {
            throw new RuntimeException("Base timestamp must be in the past");
        }

        if (workerID < 0 || workerID > MAX_WORKER_ID) {
            throw new RuntimeException("Worker ID must be between 0 and " + MAX_WORKER_ID);
        }

        this.baseTimestamp = baseTimestamp;
        this.workerID = workerID;
    }

    public synchronized long generateID() {
        // 获取当前时间戳(毫秒)
        long now = System.currentTimeMillis();
        // 计算时间戳偏移量
        long timestamp = now - this.baseTimestamp;

        // 处理时钟回拨
        if (timestamp <= this.lastTimestamp)
            waitUntilClockCatchUp(this.lastTimestamp);

        // 如果当前偏移量等于上次记录的偏移量,则说明在同一个毫秒内请求
        // 如果当前毫秒内请求次数超过4096次,则等待1毫秒
        // 否则,序列号加1
        if (timestamp == this.lastTimestamp) {
            // 这里是对序列号位进行递增并使用取模运算保证序列号不会超过4096
            this.sequence = (this.sequence + 1) & MAX_SEQUENCE;
            // 如果超出了最大值,则等待1毫秒
            if (this.sequence == 0) {
                // 获取下一毫秒
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    throw new RuntimeException(e);
                }
                now = System.currentTimeMillis();
                timestamp = now - this.baseTimestamp;
            }
        } else {
            this.sequence = 0L;
        }
        // 记录当前时间戳
        this.lastTimestamp = timestamp;
        // 返回序列号
        return (timestamp << TIMESTAMP_LEFT_SHIFT) | (this.workerID << WORKER_ID_SHIFT) | this.sequence;
    }

    private void waitUntilClockCatchUp(long lastTimestamp) {
        long currentTimestamp;
        while ((currentTimestamp = System.currentTimeMillis() - this.baseTimestamp) <= lastTimestamp) {
            long sleepTime = lastTimestamp - currentTimestamp + 1;
            try {
                Thread.sleep(sleepTime); // 等待1毫秒
            } catch (InterruptedException e) {
                // 处理中断异常
                e.printStackTrace();
                throw new RuntimeException(e);
            }
        }
    }
}