雪花算法的一些问题解析

前言

最近做项目,有些老旧项目,需要生成分布式唯一ID,不允许重复,此时如果要对其他中间件和数据库依赖小,那么就需要一套固定的ID生成规则,雪花算法就正当合适,当时Twitter就是用来存储数据库ID的,当然也可以对报文做ID,看具体的使用场景。但是坑就是使用过程中就埋下了隐患。

比如使用时间、数字长度等。 这些坑必须在SDK设计之初就有应对措施,否则很可能出现生成故障。

示例

根据github的Twitter地址:GitHub - twitter-archive/snowflake: Snowflake is a network service for generating unique ID numbers at high scale with some simple guarantees.

最开始的算法是scale写的(class类语言),原理实际上很简单

雪花算法使用long类型存储,64bit,8byte 。那么实际上数据为2的63次方,说明可能会出现负数。而且long的数字10进制位数会递增,递增随时间变化而变化,表现为开始快,后面进位慢的现象。

demo:来源于github,最初的作者已经找不到了。

package com.feng.snowflake.demo;

public class SnowMaker {
    /** 开始时间截 (这个用自己业务系统上线的时间) */
    private final long twepoch = 1704038400000L;

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

    /** 支持的最大机器id,结果是1023 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */
    private final long maxWorkerId = ~(-1L << workerIdBits);

    /** 序列在id中占的位数 */
    private final long sequenceBits = 12L;

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

    /** 时间截向左移22位(10+12) */
    private final long timestampLeftShift = sequenceBits + workerIdBits;

    /** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */
    private final long sequenceMask = ~(-1L << sequenceBits);

    /** 工作机器ID(0~1024) */
    private long workerId;

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

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

    //==============================Constructors=====================================
    /**
     * 构造函数
     * @param workerId 工作ID (0~1024)
     */
    public SnowMaker(long workerId) {
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(String.format("workerId can't be greater than %d or less than 0", maxWorkerId));
        }
        this.workerId = workerId;
    }

    // ==============================Methods==========================================
    /**
     * 获得下一个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) & sequenceMask;
            //毫秒内序列溢出
            if (sequence == 0) {
                //阻塞到下一个毫秒,获得新的时间戳
                timestamp = tilNextMillis(lastTimestamp);
            }
        }
        //时间戳改变,毫秒内序列重置
        else {
            sequence = 0L;
        }

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

        //移位并通过或运算拼到一起组成64位的ID
        return ((timestamp - twepoch) << timestampLeftShift) //时间戳位移
                | (workerId << workerIdShift) //机器码位移
                | sequence; //每次自增随机序号
    }

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

    /**
     * 返回以毫秒为单位的当前时间
     * @return 当前时间(毫秒)
     */
    protected long timeGen() {
        return System.currentTimeMillis();
    }
}

实际上,最核心的代码

return ((timestamp - twepoch) << timestampLeftShift) //
                | (workerId << workerIdShift) //
                | sequence;

时间戳位移固定22位(12位序列号+机器码10位),机器码固定位移12位。算法本身很正常,但是貌似时间戳没限制啊,很可能时间戳太大,直接占领符号位,从此变为负数,另外bit位存储是long-8字节,但是10进制可读数字,却是可变长的,正整数最大19位,负数最大20位

分析

 以上面的示例为例:以2024-1-1 00:00:00 000毫秒开始,计算10进制进位的可能性,以及69年的使用时间是怎么来的,实际上还可以负数,只是long存不下了,并不是真的只能使用69年,如果设计一种存储模式,把long的符号位加入存储,就可以存138年,实际上byte[]数组就是这么干的。

时间戳(毫秒)10进制位数10进制进位的数字值时间周期
1741943042024-01-01 00:00:00
38125829122024-01-01 00:00:00
2491006632962024-01-01 00:00:00
2391010024386562024-01-01 00:00:00
238511100034150402024-01-01 00:00:02
23842121000005959682024-01-01 00:00:23
2384191310000017653762024-01-01 00:03:58
238418614100000008765442024-01-01 00:39:44
23841858151000000003768322024-01-01 06:37:21
2384185801610000000037683202024-01-03 18:13:38
238418579217100000000041287682024-01-28 14:16:25
23841857911181000000000035389442024-10-02 22:44:17
2384185791021910000000000018350082031-07-22 11:22:59
21990232555511992233720368505815042093-09-06 15:47:35
219902325555220-92233720368547758082093-09-06 15:47:35
43980465111038-41943042163-05-15 07:35:11

可以看到雪花算法大概10个月多一点就会进位18位数字,但是在进19位时,需要7年左右,如果我们舍弃这7年,那么我们就可以得到一个固定数字位长度19位,但是只有62年左右的ID生成器。

同理69年的使用时间也是这么算出来的,因为long的设计,预留的41bit的时间戳,但是貌似没限制,如果我们代码没控制,那么69年后可以再得69年的负数。如果我们通过一个别的10进制符号位字符串标识,那么可以得到690年可以使用的不重复ID:

即20位字符串ID = 字符串0~9 + snowflake数字(最大后归0)。基本上符合绝大部分系统的设计。

存储解析

比如int的127和128和-1

可以看到127可以被byte[]的一个byte存储起来,但是128就使用了符号位,也是一个字节存储的,负数使用反码和补码来支持2进制存储,但是对于比如long,int等多个字节的byte[]存储,除了最大一位bit,其他位的byte实际上是没有符号位的,但是因为byte单个有符号位,所以,查看byte本身就使用负数存储了0~255的8bit的2进制整数。long同理,所以站在各种角度下有冲突的情况,因为所有的数据都是2进制的,反馈在输入输出流就说字节流,字符流实际上本质也是字节流。

总结

雪花算法实际上设计极为巧妙,通过时间戳,机器码,序列号(自增)来达到某个时间段(默认1毫秒)在某个并发下(并发超出自增ID就会重复或者阻塞等问题,不过我们一般达不到,且可以通过负载均衡增加资源规避),不重复ID。实现了加资源的方式来达到分布式ID不重复,且自增的特性。

但是雪花算法使用long存储,有自身限制,在以某个时间点为基线的情况,默认只能存储69年的ID,可以通过字符串扩展1位,实现600年的20位字符串ID,而且扩展的这1位可以提前的时间计算预警机制来实现进位和雪花的清0,因为我们可以精确的计算时间戳。

雪花算法高度依赖系统时间同步能力,有时间回拨的问题,这个很多解决思路。

雪花算法关键点,10进制的ID长度位数是变化的,变化的周期是可计算的,如果需要长度考虑,需要设计从19位开始,或者使用String.format("%020d", 10000000000000L)等方式,千万别认为是固定的。

雪花算法时间戳并没有限制归0,所以需要定制新的进制位字符串,或者重新更新时间戳计数基线,否则因为long的存储机制和时间戳的没限制bit,会出现负数。

  • 8
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
使用Mybatis-Plus生成雪花算法生成id非常简单。Mybatis-Plus已经内置了雪花算法生成分布式唯一id的功能。你可以在IDEA中双击shift搜索Sequence类来查看具体的实现代码。这个类使用的就是雪花算法来生成id。关于如何在项目中使用雪花算法生成id,你可以参考CSDN上的一篇博文《mybatis-plus雪花算法增强idworker》。这篇博文详细介绍了如何在Mybatis-Plus中配置和使用雪花算法生成id。你可以按照这篇博文的步骤进行操作,非常简单易懂。总结起来,使用Mybatis-Plus生成雪花算法生成id的步骤包括建表、新建测试工程和单元测试等。在实现分析中,你可以了解到为什么Mybatis-Plus默认就是使用雪花算法来生成id。此外,你还可以通过主动设置id生成策略来使用Mybatis-Plus生成雪花算法生成id。Mybatis-Plus还提供了内置的雪花算法工具类IdWorker,方便你在项目中使用雪花算法生成id。希望这些信息对你有帮助!<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [mybatis-plus雪花算法生成Id使用详解](https://blog.csdn.net/w1014074794/article/details/125604191)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值