雪花算法

分布式 id 生成算法的有很多种,Twitter 的雪花算法(nowFlake)就是其中经典的一种,它的结构如下:

  • 1bit,固定值,因为二进制中最高位是符号位,1 表示负数,0 表示正数。生成的 id 一般都用正整数,所以最高位固定为 0 。
  • 41bit,是时间戳,单位是毫秒,不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 – 开始时间截)得到的值,这里的开始时间截,一般是 id 生成器开始使用的时间,由程序来指定的,表示的范围是 0 到 (2^42) – 1,也就是说可以表示 (2^42) – 1 毫秒的值,转化为年是 69 年。
  • 10bit,机器标识,5 位机房 id 和 5 位机器 id ,10 位的长度最多支持部署 1024 个节点。
  • 12bit,计数序列号,表示能在一毫秒内生成 4096 个不同的 id 。
public class SnowFlake {

    // 开始时间截 (2023年7月22日)的某个时间点。
    private final long TWEPOCH = 1690000890436L;

    // 机器id所占的位数
    private final long WORKER_ID_BITS = 5L;

    // 数据标识(机房)id所占的位数
    private final long DATACENTER_ID_BITS = 5L;

    // 支持的最大机器id值。
    private final long MAX_WORKER_ID = (1 << WORKER_ID_BITS) - 1;

    // 支持的最大数据标识id值。
    private final long MAX_DATACENTER_ID = (1 << DATACENTER_ID_BITS) - 1;

    // 12位序列号
    private final long sequenceBits = 12L;

    // 生成序列的掩码, (0b111111111111=0xfff)
    private final long SEQUENCE_MASK = (1 << sequenceBits) - 1;

    // 机器ID(0~31)
    private long workerId;

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

    // 12位序列的值,(0~4095)
    private long sequence = 0L;

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

    /**
     * 构造函数
     *
     * @param workerId     机器id(0~31)
     * @param datacenterId 机房ID (0~31)
     */
    public SnowFlake(long workerId, long datacenterId) {
        if (workerId > MAX_WORKER_ID || workerId < 0)
            throw new IllegalArgumentException(String.format("worker Id can't be " +
                    "greater than %d or less than 0", MAX_WORKER_ID));

        if (datacenterId > MAX_DATACENTER_ID || datacenterId < 0)
            throw new IllegalArgumentException(String.format("datacenter Id can't be " +
                    "greater than %d or less than 0", MAX_DATACENTER_ID));
        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }

    /**
     * 获得下一个ID (该方法是线程安全的)
     *
     * @return SnowflakeId
     */
    public synchronized long nextId() {
        long timestamp = timeGen();// 生成当前时间的时间戳
        // 如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常。
        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) & SEQUENCE_MASK;
            if (sequence == 0) { //毫秒内序列数量超出12表示的范围。
                // 阻塞到下一个毫秒,获得新的时间戳。
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else { //不是同一时间生成的,毫秒内序列重置。
            sequence = 0L;
        }

        // 上次生成ID的时间截。
        lastTimestamp = timestamp;

        // 拼接成64为的ID。
        return ((timestamp - TWEPOCH) << 22) // 41位时间戳,要减去开始时间。
                | (datacenterId << 17) // 5位机房id
                | (workerId << 12) // 5位机器id
                | sequence;// 12位随机数
    }

    /**
     * 阻塞到下一个毫秒,直到获得新的时间戳
     *
     * @param lastTimestamp 上次生成ID的时间截
     * @return 当前时间戳
     */
    private long tilNextMillis(long lastTimestamp) {
        long curTime = timeGen();
        while (curTime <= lastTimestamp) {
            curTime = timeGen();
        }
        return curTime;
    }

    // 生成当前时间的时间戳。
    private long timeGen() {
        return System.currentTimeMillis();
    }
}

信奥赛编程(刷题请进)>>>

经过两年的打磨,我的新作《算法秘籍》已经出版,有需要的可以点击购买。也可以点击 内容介绍 查看详情。