1. 雪花算法(标准版)

雪花算法最终得到一个 long 类型的 64 位数字

雪花算法的组成部分:符号位(1) + 时间戳(41) + 数据编码(5)+ 机器编码(5)+ 序列号(12)

理论上当 机器编码 和 数据编码 不变的情况下可以生成:0 - 2^53 个 ID, 千万亿级别

代码如下:

/**
 * 雪花算法的组成部分:符号位(1) + 时间戳(41) + 数据编码(5)+ 机器编码(5)+ 序列号(12)
 * ID 生成范围: (0 - 9223372036854775808]  2^63 百亿亿级别
 * @author Mitchell
 * @since 2022-10-26
 */
public class SnowFlake {

    /**
     * 每一部分所占位数(数据中心编码,机器中心编码,序列号)
     */
    private final long datacenterIdBits = 5L;
    private final long workerIdBits = 5L;
    private final long sequenceBits = 12L;

    /**
     * 每一部分的最大值(数据中心编码,机器中心编码,序列号)
     */
    private final long maxDatacenterId = ~(-1L << datacenterIdBits);
    private final long maxWorkerId = ~(-1L << workerIdBits);
    private final long maxSequence = ~(-1L << sequenceBits);

    /**
     * 每一部分的偏移(时间戳区域,数据中心区域,机器中心区域)
     */
    private final long timestampShift = sequenceBits + datacenterIdBits + workerIdBits;
    private final long datacenterIdShift = sequenceBits + workerIdBits;
    private final long workerIdShift = sequenceBits;

    /**
     * 起始时间戳,初始化后不可修改
     */
    private final long epoch = 1690373045764L;

    /**
     * 数据中心编码,初始化后不可修改,取值范围: [0,31]
     */
    private final long datacenterId;

    /**
     * 机器进程编码,初始化后不可修改,取值范围: [0,31]
     */
    private final long workerId;

    /**
     * 序列号,取值范围: [0,4095]
     */
    private long sequence = 0L;

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

    /**
     * 雪花算法构造器
     * @param datacenterId 数据中心编码 [0,31]
     * @param workerId     机器进程编码 [0,31]
     */
    public SnowFlake(long datacenterId, long workerId) {
        if (datacenterId > maxDatacenterId || datacenterId < 0) {
            String errMsg = String.format("datacenterId 取值范围在 [0,%d] 之间", maxDatacenterId);
            throw new IllegalArgumentException(errMsg);
        }
        if (workerId > maxWorkerId || workerId < 0) {
            String errMsg = String.format("workerId 取值范围在 [0,%d] 之间", maxWorkerId);
            throw new IllegalArgumentException(errMsg);
        }
        this.datacenterId = datacenterId;
        this.workerId = workerId;
    }

    /**
     * 生成下一个唯一标识,其中:符号位 + 时间戳占 42 位
     * 1,雪花算法依赖服务器时间,如果时间发生回拨,抛异常
     * 2,每毫秒生成 maxSequence 个 id,如果超过则阻塞到下一毫秒
     */
    public synchronized long nextId() {
        long currTimestamp = timestampGen();

        if (currTimestamp < lastTimestamp) {
            String errMsg = String.format("服务器时间发生回拨 %d milliseconds", lastTimestamp - currTimestamp);
            throw new IllegalStateException(errMsg);
        }

        if (currTimestamp == lastTimestamp) {
            sequence = (sequence + 1) & maxSequence;
            if (sequence == 0) {
                currTimestamp = waitNextMillis(currTimestamp);
            }
        } else {
            sequence = 0L;
        }
        lastTimestamp = currTimestamp;

        long id = (currTimestamp - epoch) << timestampShift;
        id = id | (datacenterId << datacenterIdShift);
        return id | (workerId << workerIdShift) | sequence;
    }

    /**
     * 循环阻塞直到下一(毫秒)
     * @param currTimestamp 时间戳
     */
    protected long waitNextMillis(long currTimestamp) {
        while (currTimestamp <= lastTimestamp) {
            currTimestamp = timestampGen();
        }
        return currTimestamp;
    }

    /**
     * 获取当前时间戳
     */
    public long timestampGen() {
        return System.currentTimeMillis();
    }
}

2. 雪花算法(小数字版)

标准的雪花算法生成 id ,正常都超过了 前端 javascript 的数字范围上限,往往会丢失一部分,从而导致 ID 不一致导致系统出错

  • 解决方案1:把生成的 id 当成字符串储存,但是这会降低数据库的查询性能

  • 解决方案2:把生成的 id 当还是当成数字储存在表里,程序执行完转 json 给前端时,转化为 字符串

以上两种方式都可以解决问题,但是心中还是不爽,明明生成的是一个数字,却要和字符串转来转去,

笔者这边想了一种方式,那就是让生成的 ID 小于前端 JS 的数字上限,这样就可以愉快的使用了

这边修改了雪花算法的组成部分:符号位(1) + 时间戳(41)+ 机器编码(5)+ 序列号(7)

理论上当 机器编码 不变的情况下可以生成:0 - 2^48 个 ID, 百万亿级别

代码如下:

package com.mitchell.admin.back.service.mybatis;

/**
 * 小数字版雪花算法的组成部分:符号位(1) + 时间戳(41) + 数据编码(0)+ 机器编码(5)+ 序列号(7)
 * ID 生成范围: (0 - 9007199254740992]  2^53 千万亿
 * @author Mitchell
 * @since 2022-10-26
 */
public class SnowFlakeSmall {

    /**
     * 每一部分所占位数(机器中心编码,序列号)
     */
    private final long workerIdBits = 5;
    private final long sequenceBits = 7;

    /**
     * 每一部分的最大值(机器中心编码,序列号)
     */
    private final long maxWorkerId = ~(-1L << workerIdBits);
    private final long maxSequence = ~(-1L << sequenceBits);

    /**
     * 每一部分的偏移(时间戳区域,机器中心区域)
     */
    private final long timestampShift = workerIdBits + sequenceBits;
    private final long workerIdShift = sequenceBits;

    /**
     * 起始时间戳,初始化后不可修改
     */
    private final long epoch = 1690373045764L;

    /**
     * 机器进程编码,初始化后不可修改,取值范围: [0,31]
     */
    private final long workerId;

    /**
     * 序列号,取值范围: [0,4095]
     */
    private long sequence = 0L;

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

    /**
     * 雪花算法构造器(缩小版)
     * @param workerId 数据中心编码 [0,31]
     */
    public SnowFlakeSmall(long workerId) {
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException("无效的 machineId,其范围应该在 0 - " + maxWorkerId);
        }
        this.workerId = workerId;
    }

    /**
     * 生成下一个唯一标识,其中:符号位 + 时间戳占 42 位
     * 1,雪花算法依赖服务器时间,如果时间发生回拨,抛异常
     * 2,每毫秒生成 maxSequence 个 id,如果超过则阻塞到下一毫秒
     */
    public synchronized long nextId() {
        long currTimestamp = timestampGen();
        if (currTimestamp < lastTimestamp) {
            String errMsg = String.format("服务器时间发生回拨 %d milliseconds", lastTimestamp - currTimestamp);
            throw new IllegalStateException(errMsg);
        }
        if (currTimestamp == lastTimestamp) {
            sequence = (sequence + 1) & maxSequence;
            if (sequence == 0L) {
                currTimestamp = waitNextMillis(currTimestamp);
            }
        } else {
            sequence = 0L;
        }
        lastTimestamp = currTimestamp;
        return (currTimestamp - epoch) << timestampShift | workerId << workerIdShift | sequence;
    }

    /**
     * 循环阻塞直到下一毫秒
     * @param currTimestamp 时间戳
     */
    private long waitNextMillis(long currTimestamp) {
        while (currTimestamp <= lastTimestamp) {
            currTimestamp = timestampGen();
        }
        return currTimestamp;
    }

    /**
     * 获取当前时间戳
     */
    private long timestampGen() {
        return System.currentTimeMillis();
    }

}

3. 两相对比

标准的算法总共能生成的 ID 个数是 小数字版 的 2 ^ 5 倍,即 32 倍

标准的算法每毫秒能生成 2 ^ 12 = 4096 个 ID,小数字版本是 2 ^ 7 = 128 个

标准的算法由于同时存在 机器码 和 数据码,理论上分布式可以达到 32 * 32 = 1024 个节点,而小数字版只有 32 个

通过上方两个量级的对比,对于一般项目,小数字版已经够用,当然如果你的项目存在大量的新增,对于 ID 的需求量很大的话,还是建议使用标准版

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐