雪花算法生成分布式ID源码分析及低频场景下全是偶数的解决办法

news2024/11/23 19:24:39

目录

雪花算法原理介绍

雪花算法源码分析

低频场景下都是偶数的原因

解决雪花算法的偶数问题

1、切换毫秒时使用随机数

2、抖动上限值加抖动序列号


雪花算法原理介绍

雪花算法(snowflake)最早是twitter内部使用的分布式下的唯一id生成算法,在2014年开源,开源的版本由scala编写,地址为https://github.com/twitter-archive/snowflake,该算法具有以下特性

  • 唯一性:高并发分布式系统中生成id唯一
  • 高性能:每秒可生成百万个id
  • 有序性:生成的id是有序递增的​
  • 不依赖第三方的库或者中间件

        算法产生的是一个Long型 64 bit位的值,转换成字符串长度最长19位。

SnowFlake的结构(每部分用-分开):

0-00000000000000000000000000000000000000000-00000-00000-000000000000

  1. 第一部分:1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,ID要是正数,最高位是0
  2. 第二部分:41位毫秒数,不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 自定义起始时间截),自定义起始时间戳即人为指定的算法起始时间,当前时间即生成ID时的时间戳41位的时间截,可以使用约69年, (1L << 41) / (365 * 24 * 3600 * 1000)≈ 69
  3. 第三、四部分:10位的数据机器位,可以部署在1024(1L<<10)个节点,包括5位datacenterId(机房)和5位workerId(机器号)
  4. 第五部分:12位序列,每毫秒可生成序列号数,共4096(1L<<12)个ID序号

以上5部分总64bit,即需要一个Long整型来记录

SnowFlake的优点:

  1. 整体按时间自增排序
  2. Long整型ID,存储高效,检索高效
  3. 分布式系统内无ID碰撞(各分区由datacenterId和workerId来区分)
  4. 生成效率高,占用系统资源少,理论每秒可生成1000 * 4096 = 4096000个,也就是单台机器理论上每秒钟可以生成400万个分布式ID序列号

雪花算法源码分析

        雪花算法源码如下所示,代码里面都做了详细的说明

public class SnowFlake {

    /**
     * 数据中心/机房标识所占bit位数
     */
    private final static long DATACENTER_BIT = 5;
    /**
     * 机器标识所占bit位数
     */
    private final static long WORKER_ID_BIT = 5;
    /**
     * 每毫秒下的序列号所占bit位数,2的12次方,0到4095,一秒钟最多生产4096个数字
     */
    private final static long SEQUENCE_BIT = 12;

    // 起始时间戳 2023-07-14 00:00:00,可以根据自己的需求进行修改
    private final static long EPOCH=1689264000000L;
    // 机器标志相对序列号的偏移量 12位
    private final static long WORKER_ID_LEFT = SEQUENCE_BIT;
    // 机房标志相对机器的偏移量 17 = 12 + 5位
    private final static long DATACENTER_LEFT = WORKER_ID_LEFT + WORKER_ID_BIT;
    // 时间戳标志相对机器的偏移量 22 = 17 + 5位
    private final static long TIMESTAMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT;

    // 用位运算计算出最大支持的数据中心编号 31
    private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT);
    // 用位运算计算出最大支持的机器编号 31
    private final static long MAX_WORKER_ID_NUM = -1L ^ (-1L << WORKER_ID_BIT);
    // 用位运算计算出12位能存储的最大整数,12位的情况下为4095
    private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);
    /*
    机房 机器 序列号 上一次请求保存的时间戳
     */
    private long datacenterId;
    private long workerIdId;
    private long sequence = 0L;// 自增序列号(相当于计数器)
    private long lastStamp = -1L;

    public SnowFlake() {}
    
    /**
     * 构造函数
     *
     * @param workerId     机器ID (0~31)
     * @param datacenterId 数据中心ID (0~31)
     */
    public  SnowFlake(long workerId, long datacenterId) {
        if (workerId > MAX_WORKER_ID_NUM || workerId < 0) {
            throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", MAX_WORKER_ID_NUM));
        }
        if (datacenterId > MAX_DATACENTER_NUM  || datacenterId < 0) {
            throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", MAX_DATACENTER_NUM));
        }
        this.workerIdId = workerId;
        this.datacenterId = datacenterId;
    }
    
    /**
     * 产出下一个Id
     * @return
     */
    public synchronized long nextId() {
        // 获取当前的时间戳
        long curStamp = getCurrentStamp();
        // 若当前时间戳 < 上次时间戳则抛出异常
        if (curStamp < lastStamp) {
            throw new RuntimeException("Clock moved backwords. Refusing to generate id");
        }
        // 1.同一毫秒内
        if (curStamp == lastStamp) {
            // 1.1 相同毫秒内 id自增
            sequence = (sequence + 1) & MAX_SEQUENCE;
            // 1.2 同一毫秒内 序列数已经达到最大4095,等待下一个毫秒到来在生成
            if (sequence == 0L) {
                // 获取下一秒的时间戳并赋值给当前时间戳
                curStamp = getNextMill();
            }
        } else {
            // 2.不同毫秒,序列号重置为0
            sequence = 0L;
        }
        // 3.当前时间戳存档, 用于下次生成id对比是否为同一毫秒内
        lastStamp = curStamp;
        // 4.或运算拼接返回id
        return (curStamp - EPOCH) << TIMESTAMP_LEFT // 时间戳部分
                | datacenterId << DATACENTER_LEFT // 机房部分
                | workerIdId << WORKER_ID_LEFT // 机器部分
                | sequence; // 序列号部分
    }

    private long getNextMill() {
        long mill = getCurrentStamp();
        // 循环获取当前时间戳, 直到拿到下一秒的时间戳
        while (mill <= lastStamp) {
            mill = getCurrentStamp();
        }
        return mill;
    }

    private long getCurrentStamp() {
        return System.currentTimeMillis();
    }

    public static void main(String[] args) throws InterruptedException {
        SnowFlake snowFlake = new SnowFlake();
        for (int i = 0; i < 10; i++) {
            Thread.sleep(1L);
            System.out.println(snowFlake.nextId());
        }
    }
}

        我们运行测试一下,注意测试中每次都调用都休眠了一毫秒,来模拟低频次调用生成分布式ID的场景,运行后控制台输出如下所示:可以看到生成的都是偶数

214341257265152
214341261459456
214341269848064
214341278236672
214341286625280
214341290819584
214341295013888
214341303402496
214341307596800
214341315985408

低频场景下都是偶数的原因

        低频场景下调用时,每次都是不同毫秒值的时间戳,导致每次都走到了sequence = 0L,即序列号都是0,最终生成序列号时是通过|或运算生成的,最后一位都是0或运算得到的还是0,转成Long整数时就都是偶数了

public synchronized long nextId() {
       ......
        // 1.同一毫秒内
        if (curStamp == lastStamp) {
           ......
        } else {
            // 2.不同毫秒,序列号重置为0
            sequence = 0L;
        }
        ......
    }

        如果生成的序列号都是偶数,并且作为分库分表的分片键时就会出现严重的数据倾斜问题,这个问题是非常严重的(解决办法可以将该序列号计算hashcode再进行分片计算)

解决雪花算法的偶数问题

1、切换毫秒时使用随机数

        改动代码主要是sequence = ThreadLocalRandom.current().nextLong(randomSequenceLimit)这一行,就是在跨毫秒时自增计数器不再初始化为0,而是取一个0或者1的随机数,通过这种形式来避免都是偶数的问题。

/**
 * 此属性用于限定一个随机上限,在不同毫秒下生成序号时,给定一个随机数,避免偶数问题。
 * 注意次数必须小于{@link #MAX_SEQUENCE}
 * 不同毫秒,序列号取一个[0,randomSequenceLimit)之间的随机数,避免都是偶数的情况
 */
private final long randomSequenceLimit=2;

public synchronized long nextId() {
	......
	if (curStamp == lastStamp) {
		......
	} else {
		// 2.不同毫秒,序列号取一个[0,randomSequenceLimit)之间的随机数,避免都是偶数的情况
		sequence = ThreadLocalRandom.current().nextLong(randomSequenceLimit);
        //sequence = 0L;
	}
	......
}

2、抖动上限值加抖动序列号

  • maxVibrationOffset:最大抖动上限值,即在跨毫秒时如果sequenceOffset的值超过了maxVibrationOffset则归0,最好设置为奇数,注意该值必须小于等于MAX_SEQUENCE即4095
  • sequenceOffset:跨毫秒时的序列号,不同毫秒,超过了抖动上限则将sequenceOffset计数器归0,否则sequenceOffset累加1,sequence的值设置为sequenceOffset

        通过sequenceOffset序列号自增保证了跨毫秒时不会一直出现偶数的情况,通过maxVibrationOffset的限制保证sequenceOffset不会无限递增

//最大抖动上限值,最好设置为奇数,注意该值必须小于等于MAX_SEQUENCE即4095
private int maxVibrationOffset=1;
//跨毫秒时的序列号,跨毫秒获取时该序列号+1
private volatile int sequenceOffset = -1;

public synchronized long nextId() {
	......
	if (curStamp == lastStamp) {
		......
	} else {
		// 2.不同毫秒,处理抖动上限,超过了抖动上限则将sequenceOffset计数器归0,否则sequenceOffset累加1
        //将sequence设置为sequenceOffset
		vibrateSequenceOffset();
        sequence = sequenceOffset;
        //sequence = 0L;
	}
	......
}

private void vibrateSequenceOffset() {
    //不同毫秒时间,处理抖动上限,超过了抖动上限则将sequenceOffset计数器归0,否则sequenceOffset累加1
    sequenceOffset = sequenceOffset >= maxVibrationOffset ? 0 : sequenceOffset + 1;
}

通过测试程序进行验证:

第一种通过随机数的方式,基本上解决了跨毫秒的偶数问题,但是因为随机数的缘故,跨毫秒的奇偶数不能保证百分百的1:1,不过也可以接受;

第二种将maxVibrationOffset设置为奇数时,可以在跨毫秒时保证生成的奇偶数序列号数量为1:1,比第一种随机数的效果要好一些。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/752754.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【iOS】消息传递与消息转发

Objective-C是一门非常动态的语言&#xff0c;以至于确定调用哪个方法被推迟到了运行时&#xff0c;而非编译时。与之相反&#xff0c;C语言使用静态绑定&#xff0c;也就是说在编译期就能决定程序运行时所应该调用的函数&#xff0c;所以在C语言中&#xff0c; 如果某个函数没…

MySQL优化 | 如何正确使用索引

文章目录 一、简介1、索引的作用和优势2、索引的基本原理和数据结构 二、常见索引类型和适用场景1、B-Tree索引及其适用场景2、哈希索引及其适用场景 三、选择合适的索引策略1、 选择合适的列作为索引2、使用复合索引和最左前缀原则3、 覆盖索引的使用技巧 四、索引的创建和维护…

IDEA项目报错随笔记录

文章目录 1. 无效的源发行版: 172. java: 无法访问org.springframework.boot.SpringApplication3. java: 程序包org.junit.jupiter.api不存在4. SpringbootTest注解爆红5. maven命令安装本地jar包报错&#xff1a;[拒绝访问]5. maven命令安装本地jar包报错&#xff1a;Unknown …

华为OD机试真题 Java 实现【矩阵元素的边界值】【2023 B卷 100分】,附详细解题思路

目录 专栏导读一、题目描述二、输入描述三、输出描述四、解题思路五、Java算法源码六、效果展示1、输入2、输出3、说明4、再输入5、再输出6、再说明 专栏导读 本专栏收录于《华为OD机试&#xff08;JAVA&#xff09;真题&#xff08;A卷B卷&#xff09;》。 刷的越多&#xff…

IOS与Android APP开发的差异性

iPhone和 Android是全球最流行的两种移动平台&#xff0c;有许多不同的开发者开发了应用程序&#xff0c;并将它们发布到市场上。虽然大多数开发者都使用了这两个平台&#xff0c;但您仍然需要了解它们的差异。 虽然 iOS和 Android两个平台都是基于 Linux&#xff0c;但它们却…

流程提效80%!从3个维度搭建高效的数字化采购体系...

采购是企业经营的一个核心环节&#xff0c;也是企业获取利润和市场资源的重要部门&#xff0c;对于企业订单交付尤为关键。特别是在装备制造行业&#xff0c;项目多、零件品类多、定制化高、订单交付周期短&#xff0c;边设计边采购边生产&#xff0c;企业采购负荷重&#xff0…

读书笔记怎么写?心理学名著《乌合之众》读书笔记!

世界上这么多人&#xff0c;看似每个人是一个独立的个体&#xff0c;但其实众生如蚁&#xff0c;彼此相互影响&#xff0c;形成了复杂而神秘的群体心态。 当我们融入群体时&#xff0c;个人的特质会被群体弱化&#xff0c;变得盲从、愚昧甚至暴力&#xff0c;这就是《乌合之众》…

IP地址:超网监控

随着组织的 IT 基础架构的扩展&#xff0c;新设备会不断添加以满足不断增长的网络需求。这就需要跨多个子网管理数百个 IP 地址&#xff0c;以确保每个新添加的设备都配置了唯一的 IP 以连接到网络。为了简化此过程&#xff0c;网络管理员依赖于网络超网的概念&#xff0c;也称…

事务隔离级别是如何实现的

事务隔离级别是如何实现的 数据库系统提供了以下 4 种事务隔离级别 读未提交&#xff1a;解决了回滚覆盖类型的更新丢失&#xff0c;但可能发生脏读现象(一个事务读取到了另一个事务修改但未提交的数据)&#xff0c;也就是可能读取到其他会话中未提交事务修改的数据。 已提交…

解决报错:FUNC glfwErrorCallback GLX: Failed to create context: GLXBadFBConfig

解决报错&#xff1a;FUNC glfwErrorCallback GLX: Failed to create context: GLXBadFBConfig 执行glxinfo | grep OpenGL命令查看系统中的OpenGL信息时&#xff0c;显示以下信息&#xff1a; 根据得到的信息可以看到 OpenGL core profile version string 为 4.5 说明显卡驱…

基于web的考研信息交流平台/考研信息分享平台的设计与实现

摘 要 随着信息化时代的到来&#xff0c;管理系统都趋向于智能化、系统化&#xff0c;考研信息交流平台也不例外&#xff0c;但目前国内的有些平台仍然都使用人工管理&#xff0c;浏览网站人数越来越多&#xff0c;同时信息量也越来越庞大&#xff0c;人工管理显然已无法应对时…

【复杂系统】拥抱复杂性(第 2 部分数据)

如何通过三个简单&#xff08;ish&#xff09;步骤将您的组织网络化……从数据开始 您的信息不想被困在一个盒子里 在本文的第一部分中&#xff0c;我们确定了工业时代将复杂性组织成漂亮、整洁的线性盒子&#xff0c;尽管这是一种高效的机器制造方式&#xff0c;但这种方法已不…

cucumber基于BBD的自动化测试

1.1 BDD介绍 行为驱动开发(Behavior Driven Development&#xff0c;BDD)简历在测试驱动开发的基础上&#xff0c;并且优化了很多TDD实践者的良好习惯。BDD可以通过自然语言来描写自动化测试&#xff0c;增加自动化的可阅读性. 1.2 cucumber原理 reshen目前有很多BDD的框架&a…

达观曹植大模型正式对外公测!专注于长文本、多语言、垂直化发展

大模型时代到来&#xff0c;国内出现“百模大战”的局面。达观数据自23年3月宣布研发大语言模型以来&#xff0c;一直积极探索大语言模型的专业化、特长化和产品化。通过多年的高质量数据积累&#xff0c;不断精进算法创新&#xff0c;结合多年的文本处理工程实践经验&#xff…

【C】自定义类型详解

自定义类型 结构体结构体类型声明特殊的声明 结构的自引用结构体的定义和初始化结构体的内存对齐为什么存在内存对齐呢&#xff1f;修改默认的对齐参数 结构体传参位段位段的内存分配位段的跨平台问题 枚举枚举类型的定义枚举的优点枚举的使用 联合&#xff08;共用体&#xff…

刷题记录03

题目一. 具体思路: 这里就是,一个简单的解方程的一个思路,首先我们要理清楚,这道题,这里面的思路 A-Ba B-Cb ABc BCd 由上面的关系得出下面的解 A(a c)/2 B1(bd)/2 B2(c-a)/2 C(b-d)/2 具体代码: import java.util.Scanner;// 注意类名必须为 Main, 不要有任何 package xxx 信…

maven-依赖管理-上

依赖管理 依赖配置 一句话: 依赖指当前项目运行需要的jar&#xff0c;一个项目可以设置多个依赖 依赖的举例 <!-- 解读: 1. 这里就是引入项目需要的jar 包, 类似传统的import jar 的作用 2. 在dependencies 标签内&#xff0c;可以指定多个需要依赖的jar/导入的jar 3. 引…

Skywalking 9.4部署监控Java应用链路跟踪

资源地址&#xff1a; github地址&#xff1a;GitHub - apache/skywalking: APM, Application Performance Monitoring System java agent下载地址&#xff1a;Index of /dist/skywalking/java-agent 一.监控架构图 二.使用docker-compose启动skywalking以及ES version: 3.3…

字符函数和内存函数 (一)

目录 一、strlen函数 1.1strlen函数的认识 1.2strlen函数的模拟实现 二、strcpy函数 2.1strcpy函数的认识 2.2strcpy函数的模拟实现 三、strcat函数 3.1strcat函数的认识 3.2strcat函数的模拟实现 四、strcmp函数 4.1strcmp函数的认识 4.2strcmp函数的模拟实现 五、…

Java 面试知识点

Java 面试知识点 Java基础知识1. 一个".java"源文件中是否可以包括多个类&#xff08;不是内部类&#xff09;&#xff1f;有什么限制&#xff1f;2. 说说&和&&的区别。3.在 JAVA 中如何跳出当前的多重嵌套循环&#xff1f;4.switch 语句能否作用在 byte…