Java分布式ID-雪花算法

首选我们需要明确的结论:雪花算法生成分布式ID是互联网公司常用的算法,所以可以大胆放心的使用。但是雪花算法也会存在问题,比如时间回拨会造成生成重复的ID,但是这种情况毕竟是小概率事件,不必特殊考虑,或者如果真的发生了,我们要有对应的解决方案。还是那句话,没有完美的解决方案,我们不能因噎废食。

1、首先我们来了解一下雪花算法生成的分布式id结构,了解结构之后我们对于代码才能有更深的理解。

                                                          雪花算法生成分布式ID组成图

1.1 1bit:最高位关于整个数字是正还是负。 1 -代表数字位负数 0-代表正数。

1.2 41bit: 存储毫秒级时间戳(现在时间戳-1970年得出得时间戳),2^41/(1000*60*60*24*365)=69,大概可以使用 69 年。那么从1970开始算的话,那么大约可以使用到2039年左右,之后可能会出现重复的情况。

1.3 10bit: 存储机器码,包括 5 位 datacenterId(数量没有那么大的话可以直接根据业务来区分) 和 5位workerId。最多可以部署 2^10=1024 台机器。

1.4 12bit: 存储序列号。同一毫秒时间戳时,通过这个递增的序列号来区分。即对于同一台机器而言,同一毫秒时间戳下,可以生成 2^12=4096 个不重复 id。

2、那么通过上面结构的介绍,我们可以用代码实现,主要看getNextId方法,一定要先看上面的思路,搞懂整个分布式ID串的结构,然后再看代码,这一样更容易理解。

public  class  SnowFlakeUtil {

    // 初始时间戳(纪年),可用雪花算法服务上线时间戳的值
    // 1650789964886:2022-04-24 16:45:59
    private static final long INIT_EPOCH = 1650789964886L;
    // 记录最后使用的毫秒时间戳,主要用于判断是否同一毫秒,以及用于服务器时钟回拨判断
    private long lastTimeMillis = -1L;
    // 最后12位,代表每毫秒内可产生最大序列号,即 2^12 - 1 = 4095
    private static final long SEQUENCE_BITS = 12L;
    // 掩码(最低12位为1,高位都为0),主要用于与自增后的序列号进行位与,如果值为0,则代表自增后的序列号超过了4095
    // 0000000000000000000000000000000000000000000000000000111111111111
    private static final long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);
    // 同一毫秒内的最新序号,最大值可为 2^12 - 1 = 4095
    private long sequence;
    /**
     * 通过该方法生成分布式ID
     *   整个算法的流程:
     *     1.先获取当前时间戳到毫秒。
     *     2.当前时间戳-INIT_EPOCH时间(防止41位空间浪费)
     *     3.查看当前日期是否同一毫秒。是统一毫秒直接相加。不是同一毫秒的话直接新增。
     *     4.如果当前时间戳内生成的ID超过了4095,那么可以循环阻塞等待下一1毫秒在生成。
     *     5.如果服务器时间出现问题,回退了,这抛出异常。(时间回拨的问题)
     * @param dataCenterId  机器中心ID
     * @param workerId      服务器ID
     * @return
     */
    public synchronized long getNextId(long dataCenterId, long workerId) {
        // 获取当前毫秒
        long currentTimeMillis = System.currentTimeMillis();
        // 5.如果服务器时间出现问题,回退了,这抛出异常。(时间回拨的问题)
        if (currentTimeMillis < lastTimeMillis) {
            throw new RuntimeException(
                    String.format("可能出现服务器时钟回拨问题,请检查服务器时间。当前服务器时间戳:%d,上一次使用时间戳:%d", currentTimeMillis,
                            lastTimeMillis));
        }
        if (currentTimeMillis == lastTimeMillis) {
            //4095+1 和 SEQUENCE_MASK & 得出的结果是0。
            sequence = (sequence + 1) & SEQUENCE_MASK;
            if (sequence == 0) {
                //4.如果当前时间戳内生成的ID超过了4095,那么可以循环阻塞等待下一1毫秒在生成。
                currentTimeMillis = getNextMillis(lastTimeMillis);
            }
        } else { // 不在同一毫秒内,则序列号重新从0开始,序列号最大值为4095
            sequence = 0;
        }
        // 记录最后一次使用的毫秒时间戳
        lastTimeMillis = currentTimeMillis;
        // 时间戳部分,currentTimeMillis - INIT_EPOCH 和 因为什么要左移,具体原因可以看我的上一篇Redis生成ID中有详细的解释
        return ((currentTimeMillis - INIT_EPOCH) << 22)
                        // 数据中心部分
                        | (dataCenterId << 17)
                        // 机器表示部分
                        | (workerId << 12)
                        // 序列号部分
                        | sequence;
    }

    /**
     * 获取指定时间戳的接下来的时间戳,也可以说是下一毫秒
     * @param lastTimeMillis 指定毫秒时间戳
     * @return 时间戳
     */
    private long getNextMillis(long lastTimeMillis) {
        long currentTimeMillis = System.currentTimeMillis();
        while (currentTimeMillis <= lastTimeMillis) {
            currentTimeMillis = System.currentTimeMillis();
        }
        return currentTimeMillis;
    }
}

3、雪花算法的优缺点

优点:不依赖第三方库或者中间件,直接java代码生成;每秒可以生成百万个不重复的ID(一般数据量没有那么大);可以保证生成的ID自增;在内存中生成,效率高。

缺点: 依赖服务器时间,服务器时钟回拨时可能会生成重复 id。

4、雪花算法在实际项目中真正的用法

4.1 可以把雪花算法部署在和服务相同的机器上,这样对应的传入参数dataCenterId(我们可以根据业务来区分)和workerId(对应的每个机器编码)。

4.2 可以把雪花算法单独部署微服务和真正的服务器分开,如下图。

但是这里会出现问题:

4.3.1 不同的业务使用相同的序列号,这样就会造成分布式ID重复,并且还可能超过每毫秒4095的极限。

举例如下: 订单 001 支付服务 002 ,最终会造成 001 01, 002 02,00103,000204。这样的序列。这样会造成每个业务的id不连续。随着业务的逐渐增加,可能会造成每毫秒4095的极限。

解决方案:我们可以给代码中增加每个业务类型 lastTimeMillis。增加map结构key:业务类型 value:lastTimeMillis来解决。

4.3.2 4.2将雪花算法单独增加服务器,这样我们的确实实现了高可用,高并发。但是还是有问题,我们每个服务中的dataCenterId和workerId必须自己单独设置和真正的服务无关。不然还可能出现重复的情况。针对这种情况,1)我们可以将每台的hostname和ip地址作为dataCenterId和workerId。2)引入第三方,比如增加zookeeper来启动的时候自动分配dataCenterId和workerId。

5、时间回拨的问题

在实际生产中,服务器事件回拨的问题属于小概率事件。一般情况下我们不需要考虑这样的问题。所以到这时候,你就可以结束了。代码写完直接部署运行就行,不用考虑那么多。

但是有的时候:官方的话,为了系统的稳定性,我们也需要设置对应得解决方案。这样才能保证系统得稳定性,私下里,其实是有些人专牛角尖,或者面试的时候,面试官非要你说出个456来。那么记得了解一下这样的解决方案。

5.1 那么如果时间非常短。比如回拨时间在<5毫秒,我们可以利用代码中得方案。直接循环阻塞直到经过5毫秒。然后继续生成。

5.2 如果大于1秒小于10毫秒了,那么我们可以把最最近的10毫秒,存储在内存的map存最近10毫秒的最大值,然后这个上面直接增加。

5.3 如果直接大约10毫秒了,或者很大,那怎么办,按照我的意思是直接抛出异常,直接报警发送邮件,然后运维手动处理服务器时间,然后重新启动服务就行了。

5.4 如果服务器不是高可用的咋办,一停的话直接真个服务都挂了,咋办,那么我们可以直接保存这样的数据到数据库或者mq,然后通过调用批量或者mq直接处理。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值