雪花算法的一些问题解析

news2024/11/13 12:45:11

前言

最近做项目,有些老旧项目,需要生成分布式唯一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,会出现负数。

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

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

相关文章

服务器主机安全有多重要

一、什么是主机安全 主机安全&#xff0c;作为维护计算机系统核心安全的基石&#xff0c;旨在全面捍卫硬件与软件免受任何未经授权的侵扰、篡改、数据泄露等安全挑战。这一过程不仅聚焦于数据存储与处理的保密性、完整性及可用性&#xff0c;还深入至硬件构造、固件层、以及系…

利用C++11的异步操作实现一个线程池

利用C11的异步操作实现一个线程池 利用C11的异步操作实现一个线程池 介绍关于一些代码细节的解释测试 介绍 基于线程池执行任务的时候&#xff0c;入口函数内部执行逻辑是固定的&#xff0c;因此选择std::packaged_task加上std::future的组合来实现。 具体使用可以见我上一…

2025年第7届图像处理和机器视觉国际会议 (IPMV 2025)即将召开!

2025年第7届图像处理和机器视觉国际会议 (IPMV 2025)将于2025年1月10日-12日在中国香港举行。图像处理和机器视觉作为当代信息技术领域的重要分支&#xff0c;不仅推动了人工智能技术的飞速发展&#xff0c;也为各行各业带来了革命性的变革。本次会议旨在汇聚全球图像处理和机器…

极致的灵活度满足工程美学:用Vue Flow绘制一个完美流程图

目录 极致的灵活度满足工程美学&#xff1a;用Vue Flow绘制一个完美流程图 一、环境要求 二、初识Vue Flow 2.1、安装Vue Flow 2.2、Vue Flow构成 2.3、一个小坑 2.4、入门案例 三、Vue Flow优秀的自定义功能 3.1、引入 3.2、节点与连线的自定义 ①打样&#xff08;…

MySQL - 通过SQL语句导出数据到CSV文件

在 MySQL 中&#xff0c;可以使用 SELECT ... INTO OUTFILE 语句将查询结果导出为 CSV 文件&#xff0c;然后再将 CSV 文件转换为 Excel 格式。以下是一个示例&#xff1a; SELECT column1, column2, column3 INTO OUTFILE /path/to/file.csv FIELDS TERMINATED BY , ENCLO…

git 迁移仓库的方法

git Git是一个开源的分布式版本控制系统&#xff0c;由Linus Torvalds在2005年创建&#xff0c;用于有效、高速地处理从小到大的项目管理。它最初是为Linux内核开发而设计的&#xff0c;但很快被广泛用于各种项目。 以下是Git的一些主要特性&#xff1a; 分布式架构&#xff…

接近传感器 - 从零开始认识各种传感器【第十七期】

1、什么是接近传感器 接近传感器常被用于检测物体的距离或者是否有物体靠近。它通常发射电磁场或电磁波&#xff08;例如红外线&#xff09;来探测物体的位置。当有物体靠近时&#xff0c;传感器会接收到反射的电磁波信号&#xff0c;从而触发相应的电路或者控制系统。它广泛应…

Helm(二)

一、Chart模板流程控制if_with_range 1.if 修改values.yaml cat > values.yaml <<EOF myname: yeunyi service: type: ClusterIP port: 80 myport: 8080 EOF 修改service.yaml cat > templates/service.yaml <<EOF apiVersion: v1 kind: Service met…

TC8:SOMEIP_ETS_007-008

SOMEIP_ETS_007: echoBitfields 目的 检查位字段是否能够被顺利地发送和接收。 测试步骤 Tester:创建SOME/IP消息Tester:使用method echoBitfields发送SOME/IP消息DUT:返回method响应消息,其中位字段的顺序与请求相比是反向的期望结果 3、DUT:返回method响应消息,其中位…

微软蓝屏”事件暴露了网络安全哪些问题?

&#x1f4e2;博客主页&#xff1a;https://blog.csdn.net/2301_779549673 &#x1f4e2;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01; &#x1f4e2;本文由 JohnKi 原创&#xff0c;首发于 CSDN&#x1f649; &#x1f4e2;未来很长&#…

探秘数字孪生技术在智慧校园的应用

在当今教育信息化的浪潮中&#xff0c;数字孪生技术逐渐成为智慧校园建设的重要助推器。这一技术通过构建真实世界的数字化映像&#xff0c;不仅提升了校园管理的效率&#xff0c;更为师生的日常学习生活提供了全新的体验。首先&#xff0c;数字孪生可以将校园内的各类设施、环…

七夕告白攻略:天使智能体教你如何设计完美表白卡片!独属程序员地浪漫!

文章目录 &#x1f495;七夕浪漫告白天使&#x1f495;&#x1f495;浪漫风格的表白卡片设计&#x1f495;&#x1f495;甜蜜风格的表白卡片设计&#x1f495;&#x1f495;温馨风格的表白卡片设计&#x1f495;&#x1f495;幽默风格的表白卡片设计&#x1f495;&#x1f495;…

使用java读取本地文件内容并输出,java读取文件内容,节省内存开销

java使用FileInputStream读取本地文件内容 java使用Stream流读取本地文件内容 1.先在自己笔记本选一个目录创建文件&#xff0c;这里就选择在D盘创建一个 word.txt文件 随意输入内容例如 2.直接来直接复制代码运行 import java.io.*; import java.nio.file.Files; import ja…

rust sip电话(基于tauri 、pjsip库)

本文尝试下rust 的tauri 桌面运用 原因在于体积小 1、pjsip 提供了rust 接口官方的 rust demo 没编译出来 在git找了个sip-phone-rs-master https://github.com/Charles-Schleich/sip-phone-rs 可以自己编译下pjsip lib库替换该项目的lib 2、创建一个tauri demo 引用 [depe…

佐糖v1.7.2高级版,全能型AI图片处理App!

经常刷抖音的小伙伴应该都遇见过这种吧&#xff0c;修复老张片一张10块钱&#xff0c;老照片还原高清...这种看似很高端很神器&#xff0c;你以为真的有画师专门帮你来修复老张片吗&#xff1f;&#xff01;怎么可能...都是现在AI技术识别照片信息&#xff0c;然后给你生成的。…

全国禁毒知识竞赛--“我是答题王”竞赛规则

全国禁毒知识竞赛共分四轮&#xff0c;通过6支队伍最终分数高低&#xff0c;角逐出团队一、二、三等奖。第四轮各队派出1名代表进行车轮挑战&#xff0c;最终决出本届的“我是答题王”一、二、三等奖和个人答题王。 环节一&#xff1a;火线缉毒&a…

基于CNN的手写汉字识别系统的设计与实现

1 项目介绍 1.1 摘要 随着数字化技术的快速发展&#xff0c;手写汉字识别技术在多个领域展现出广泛的应用前景。为了提升手写汉字识别的准确率和泛化能力&#xff0c;本文通过构建一个基于卷积神经网络&#xff08;CNN&#xff09;的手写汉字识别系统&#xff0c;以应对不同书…

Nginx系列-11 HTTP消息处理流程

背景 了解Nginx处理HTTP请求的11个阶段&#xff0c;有助于理解和配置nginx、自定义模块、基于lua模块自定义功能。按如下配置&#xff0c;执行"curl http://localhost:8001/query/test.html"&#xff0c;如果读者对结果不是很确定&#xff0c;建议阅读本文。 serve…

免费pdf转word软件有哪些?5个软件帮助你快速进行文件转换

免费pdf转word软件有哪些&#xff1f;5个软件帮助你快速进行文件转换 将PDF文件转换为Word格式可以方便编辑和修改。以下是五款免费的PDF转Word软件&#xff0c;它们可以帮助你快速进行文件转换。 迅捷PDF转换器 这是一款设计简洁、操作便捷的PDF工具&#xff0c;它集成了多…

go语言day20 使用gin框架获取参数 使用自定义的logger记录日志

Golang 操作 Logger、Zap Logger 日志_golang zap-CSDN博客 目录 一、 从控制器中获取参数的几种形式 1&#xff09; 页面请求url直接拼接参数。 2&#xff09; 页面请求提交form表单 3&#xff09; 页面请求发送json数据&#xff0c;使用上下文对象c的BindJSON()方法接…