雪花算法改造失败导致ID重复问题分享

背景

雪花算法是分布式应用中应用比较多的 ID 生成算法,某项目中使用该算法生成ID,近期被反馈算法生成的 ID 存在重复的情况,排了一天,终于找到问题根源了。

本文将总结这个 Bug ,顺便温故一下雪花算法及改造雪花算法支持更大的工作中心和机器码的注意事项。

雪花算法概览

雪花算法 (SnowFlake ),是 Twitter 开源的分布式 id 生成算法。 为什么叫雪花算法呢?据相关研究表示,一般的雪花大约由10的19次方个水分子组成。在雪花形成过程中,会形成不同的结构分支,所以说大自然中不存在两片完全一样的雪花,每一片雪花都拥有自己独特的形状。 雪花算法,顾名思义,算法生成的ID如雪花一般独一无二。

其核心思想是:使用一个 64 bit 的 long 型的数字作为全局唯一 id。在分布式系统中的应用十分广泛,且ID 引入了时间戳,基本上保持自增的。

雪花算法的 64 位数据组成结构如下《原图来源》

中间 10位=数据中心5位 + 机器码5位,最后12位存同一毫秒内的ID序号。 最后这三部分能表示的最大数值为:

bash 复制代码
数据中心个数:0-31
机器码个数:0-31
序号数:0-4095

算法可以保证一个工作中心的一台机器上,在同一毫秒内,生成一个唯一的 id。 同一毫秒内,由最后 12 位的序号累加生成不同 ID。

算法改造过程

项目对该算法做了改造,希望能配置更大的数据中心和机器码,所以把后三部分的长度调整为:

  1. 时间戳:37位。
  2. 数据中心:9位,范围 0-511。
  3. 机器码:7位,范围 0-127。
  4. 相同毫秒内的序列号:10,范围 0-1024。

这个改造算法出现了 ID 重复的情况,打印某次测试时 ID 及当时的时间信息如下:

bash 复制代码
id=5698338316999465984,timestamp=1720607857794,dataCenterId=300,workId=7,sequence=0
id=5698338316999465984,timestamp=1720607857798,dataCenterId=300,workId=7,sequence=0

按上面的配置,执行算法的移位操作:

java 复制代码
long start = 1720607857794L;
long a = 1720607857794L;
long b = 1720607857798L;

long dataCenterId = 300;
long workId = 7;
long id1 =  (a - start) << 26 // 时间戳部分
        | dataCenterId << 20       //数据中心部分
        | workId << 10             //机器标识部分
        | 0;

long id2 =  (b - start) << 26 //时间戳部分
        | dataCenterId << 20       //数据中心部分
        | workId << 10             //机器标识部分
        | 0;

按日志输出的重复ID时的信息,改造的算法还原数据执行结果为:

确实出现了 ID 重复问题,反复比对测试,终于找到了根源,数据中心移位值计算错误 。

java 复制代码
private short workIdBits = 7;
private short sequenceBits = 10;
/**
 * wordId节点id向左移10位
 */
private short workIdShift = sequenceBits;
/**
 * 数据中心id向左移
 */
private short dataCenterIdShift = (short)(sequenceBits + workIdShift);

实际上,数据中心移位值应该序号位数+主机ID位数,算法却写成了 序号位数+主机ID移位数=20 了 。

改正代码为:private short dataCenterIdShift = (short)(sequenceBits + workIdBits);

按这个扩展思路,修正数据中心移位值后,生成的 ID 就不存在重复问题了,只是 数据中心+工作机器码占16 位后,序列号10位,每毫秒只能生成 1024 个 ID 了,同时能使用的时间长度变小了。

雪花算法实现类

雪花算法实现不过一百多行,还是比较简单的,到处都能搜到,《常见源码如下》

bash 复制代码
public class IdWorker { 
    //因为二进制里第一个 bit 为如果是 1,那么都是负数,但是我们生成的 id 都是正数,所以第一个 bit 统一都是 0。
    //机器ID  2进制5位  32位减掉1位 31个
    private long workerId;
    //机房ID 2进制5位  32位减掉1位 31个
    private long datacenterId;
    //代表一毫秒内生成的多个id的最新序号  12位 4096 -1 = 4095 个
    private long sequence;
    //设置一个时间初始值    2^41 - 1   差不多可以用69年
    private long twepoch = 1585644268888L;
    //5位的机器id
    private long workerIdBits = 5L;
    //5位的机房id
    private long datacenterIdBits = 5L;
    //每毫秒内产生的id数 2 的 12次方
    private long sequenceBits = 12L;
    // 这个是二进制运算,就是5 bit最多只能有31个数字,也就是说机器id最多只能是32以内
    private long maxWorkerId = -1L ^ (-1L << workerIdBits);
    // 这个是一个意思,就是5 bit最多只能有31个数字,机房id最多只能是32以内
    private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
 
    private long workerIdShift = sequenceBits;
    private long datacenterIdShift = sequenceBits + workerIdBits;
    private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
    private long sequenceMask = -1L ^ (-1L << sequenceBits);
    //记录产生时间毫秒数,判断是否是同1毫秒
    private long lastTimestamp = -1L;
    public long getWorkerId(){
        return workerId;
    }
    public long getDatacenterId() {
        return datacenterId;
    }
    public long getTimestamp() {
        return System.currentTimeMillis();
    }
 
    public IdWorker(long workerId, long datacenterId, long sequence) {
 
        // 检查机房id和机器id是否超过31 不能小于0
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(
                    String.format("worker Id can't be greater than %d or less than 0",maxWorkerId));
        }
 
        if (datacenterId > maxDatacenterId || datacenterId < 0) {
 
            throw new IllegalArgumentException(
                    String.format("datacenter Id can't be greater than %d or less than 0",maxDatacenterId));
        }
        this.workerId = workerId;
        this.datacenterId = datacenterId;
        this.sequence = sequence;
    }
 
    // 这个是核心方法,通过调用nextId()方法,让当前这台机器上的snowflake算法程序生成一个全局唯一的id
    public synchronized long nextId() {
        // 这儿就是获取当前时间戳,单位是毫秒
        long timestamp = timeGen();
        if (timestamp < lastTimestamp) {
 
            System.err.printf(
                    "clock is moving backwards. Rejecting requests until %d.", lastTimestamp);
            throw new RuntimeException(
                    String.format("Clock moved backwards. Refusing to generate id for %d milliseconds",
                            lastTimestamp - timestamp));
        }
 
        // 下面是说假设在同一个毫秒内,又发送了一个请求生成一个id
        // 这个时候就得把seqence序号给递增1,最多就是4096
        if (lastTimestamp == timestamp) {
 
            // 这个意思是说一个毫秒内最多只能有4096个数字,无论你传递多少进来,
            //这个位运算保证始终就是在4096这个范围内,避免你自己传递个sequence超过了4096这个范围
            sequence = (sequence + 1) & sequenceMask;
            //当某一毫秒的时间,产生的id数 超过4095,系统会进入等待,直到下一毫秒,系统继续产生ID
            if (sequence == 0) {
                timestamp = tilNextMillis(lastTimestamp);
            }
 
        } else {
            sequence = 0;
        }
        // 这儿记录一下最近一次生成id的时间戳,单位是毫秒
        lastTimestamp = timestamp;
        // 这儿就是最核心的二进制位运算操作,生成一个64bit的id
        // 先将当前时间戳左移,放到41 bit那儿;将机房id左移放到5 bit那儿;将机器id左移放到5 bit那儿;将序号放最后12 bit
        // 最后拼接起来成一个64 bit的二进制数字,转换成10进制就是个long型
        return ((timestamp - twepoch) << timestampLeftShift) |
                (datacenterId << datacenterIdShift) |
                (workerId << workerIdShift) | sequence;
    }
 
    /**
     * 当某一毫秒的时间,产生的id数 超过4095,系统会进入等待,直到下一毫秒,系统继续产生ID
     * @param lastTimestamp
     * @return
     */
    private long tilNextMillis(long lastTimestamp) {
 
        long timestamp = timeGen();
 
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }
    //获取当前时间戳
    private long timeGen(){
        return System.currentTimeMillis();
    }
}

启示录

这个问题也是偶然发生的,以前这个数据中心数值固定为0,但是集群部署时,如果忘记修改这个值,总是出现 ID 重复问题。

后来做了一次脚本优化,把数据中心配置项设置为 【0-511】 范围的随机��了。在数据中心值为 300 时,且1秒内、密集的生成 ID 、且有毫秒及的时间差时,这个代码缺陷才被发现。

正常数据中心应该左移17位,但是写错为 20时,当数据中心为 300时,整个数据中心高位非0时,上图会覆盖时间戳部分的3位数据,从而导致 ID 重复。

这世间的 Bug 啊,真是环环相扣!

思考一个问题:雪花算法 32个工作中心、32台机器的这种默认配置,能否满足分布式环境下的 ID 生成的需求呢?1024 台机器的集群规模、每毫秒 4096 个 ID ,够不够用呢?

应该是够的,如果不够,可以调整对应三部分的长度进行扩大,但是需要注意找正确的算法进行修改。

相关推荐
qq_4419960520 分钟前
Mybatis官方生成器使用示例
java·mybatis
巨大八爪鱼27 分钟前
XP系统下用mod_jk 1.2.40整合apache2.2.16和tomcat 6.0.29,让apache可以同时访问php和jsp页面
java·tomcat·apache·mod_jk
码上一元2 小时前
SpringBoot自动装配原理解析
java·spring boot·后端
计算机-秋大田2 小时前
基于微信小程序的养老院管理系统的设计与实现,LW+源码+讲解
java·spring boot·微信小程序·小程序·vue
魔道不误砍柴功4 小时前
简单叙述 Spring Boot 启动过程
java·数据库·spring boot
失落的香蕉4 小时前
C语言串讲-2之指针和结构体
java·c语言·开发语言
枫叶_v4 小时前
【SpringBoot】22 Txt、Csv文件的读取和写入
java·spring boot·后端
wclass-zhengge4 小时前
SpringCloud篇(配置中心 - Nacos)
java·spring·spring cloud
路在脚下@4 小时前
Springboot 的Servlet Web 应用、响应式 Web 应用(Reactive)以及非 Web 应用(None)的特点和适用场景
java·spring boot·servlet
黑马师兄4 小时前
SpringBoot
java·spring