分布式ID-一窥雪花算法的原生实现问题与解决方案(CosId)

news2024/9/20 14:24:42

分布式ID-雪花算法的问题与方案(CosId)

基本原理

在这里插入图片描述

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=%E5%88%86%E5%B8%83%E5%BC%8FID-%E9%9B%AA%E8%8A%B1%E7%AE%97%E6%B3%95%E7%9A%84%E9%97%AE%E9%A2%98%E4%B8%8E%E6%96%B9%E6%A1%88%EF%BC%88CosId%EF%BC%89_image.&pos_id=img-SZPkqRew-1724123351152)

Snowflake算法的原理相对直观,它有不同的部分组成,每个部分单独来看可能会导致重复,但是组合在一起做到全局唯一。

它负责生成一个64位(long型)的全局唯一ID,这个ID的构成包括:1位无用的符号位, 41位的时间戳, 10位的机器ID. 以及12位的序列号,除了固定的1位符号位之外,其余的三个部分都可以根据实际需求进行调整:

  1. 41位时间戳=(1L<<41)/(1000/3600/24/365):这部分能够表示的时间跨度大约69年。即可以使用的绝对时间为EPOCH+69年,一般我们需要自定义EPOCH为产品开发时间,另外还可以通过压缩其他区域的分配位数,来增加时间戳位数来延长可用时间。
  2. 10位工作进程ID=(1L<<10)=1024:时间戳可以保证单台机器单调递增不重复,但是如果是不同机器的集群呢?那么就有可能产生相同的时间戳。这时候就可以把进程ID给拼接上来,机器ID可以唯一标识最多1024个相同的业务。
  3. 12位自增序列号=(1L<<12)*1000=4096000:如果在同一个进程中有多个线程同时生成,那么还是会产 生相同的ID,怎么办?那就再加上一个严格递增的序列位。这样就整体保证了全局的唯一性。

存在的问题

时间戳的坑:时钟回拨问题

服务器时钟回拨是由于在某些情况下,服务器的系统时钟会发生不可避免或人为的变化,在高并发场景下, 获得的高精度时间戳,有时候会往前跳,有时候又会往回拨。一旦时钟往回拨,就有可能产生重复的ID,这 就是时钟回拨问题。

解决的方法有很多,雪花算法对此并没有标准解决方案,不同框架有自己的解决方法,但是基本思路都是用上一次生成主键的时间戳,然后拿当前时间和上一次的时间进行比较,只是发现有问题后的解决方式会有不同:

  • shardingsphere解决方案:如果出现回拨(当前时间小于上一次获取的时间),当前线程就暂时sleep一小段时间,然后重新获取时间戳。
    @SneakyThrows(InterruptedException.class)
    private boolean waitTolerateTimeDifferenceIfNeed(final long currentMillis) {
        if (lastMillis.get() <= currentMillis) {
            return false;
        }
        long timeDifferenceMillis = lastMillis.get() - currentMillis;
        ShardingSpherePreconditions.checkState(timeDifferenceMillis < maxTolerateTimeDifferenceMillis,
                () -> new AlgorithmExecuteException(this, "Clock is moving backwards, last time is %d milliseconds, current time is %d milliseconds.", lastMillis.get(), currentMillis));
        Thread.sleep(timeDifferenceMillis);
        return true;
    }
  • CosId框架发现时钟回拨直接抛出异常。
AbstractSnowflakeId

long currentTimestamp = getCurrentTime();
if (currentTimestamp < lastTimestamp) {
   throw new ClockBackwardsException(lastTimestamp, currentTimestamp);
}
  • 使用ntpd这样的时间同步服务。
  • 美团的Leaf服务:时间戳不依赖本地的服务,放在第三方服务统一管理和获取,省却了时间同步的麻烦,但是因为会依赖网络通信,从而产生IO效率和可用性问题。

工作进程ID如何分配问题

SnowflakeId中根据业务设计的位分配方案确定了基本上就不再有变更了,也很少需要维护。但是工作进程ID总是需要配置的,而且集群中是不能重复的,还要考虑服务重启后分配ID保持稳定性,否则分区原则就会被破坏而导致ID唯一性原则破坏,当集群规模较大时工作进程ID的维护工作是非常繁琐,低效的。

COSID提供的方案如下:

MachineIdDistributorSnowflakeId 的机器号分配器,它负责分配机器号,同时还会存储MachineId的上一次时间戳,用于启动时时钟回拨的检查。

目前 CosId 提供了以下六种 MachineId 分配器。

  • ManualMachineIdDistributor: 手动配置machineId,一般只有在集群规模非常小的时候才有可能使用,不推荐。
  • StatefulSetMachineIdDistributor: 使用KubernetesStatefulSet提供的稳定的标识ID(HOSTNAME=service-01)作为机器号。
  • RedisMachineIdDistributor: 使用Redis作为机器号的分发存储,同时还会存储MachineId的上一次时间戳,用于启动时时钟回拨的检查。
  • JdbcMachineIdDistributor: 使用关系型数据库作为机器号的分发存储,同时还会存储MachineId的上一次时间戳,用于启动时时钟回拨的检查。
  • ZookeeperMachineIdDistributor: 使用ZooKeeper作为机器号的分发存储,同时还会存储MachineId的上一次时间戳,用于启动时时钟回拨的检查。
  • MongoMachineIdDistributor: 使用MongoDB作为机器号的分发存储,同时还会存储MachineId的上一次时间戳,用于启动时时钟回拨的检查。
    在这里插入图片描述

对于实例应用分成两类,一类是stable应用,就是稳定的应用,一类是不稳定的应用。以JdbcMachineIdDistributor分发器为例:

  • 不稳定的应用会回收机器号。每个新应用启动时在cosid_machine表就会有一条记录,并把分配的机器号写到machine_id字段,那么应用实例怎么跟这个机器号关联呢?这条记录还有一个instance_id字段(默认为ip:pid), 当这个应用设置成不稳定的应用时,instance_id字段在写入后暂时与分配的机器号形成了关联关系,然而到应用停止时,Spring的SmartLifecycle回调会回收这个关系(清空这条记录的instance_id字段),这条记录也不是不再用了,它会等待其它应用启动时重新回收利用(重新写入instance_id字段以建立关联关系)。
  • 稳定的应用相比不稳定的应用就是应用停止时不会有回收的动作,并且在本地的.cosid-machine-state目录会保存当前应用的机器号和时间戳,下次启动时还是会找到同一条记录。

下图展示了CosId分配工作进程id的过程:
在这里插入图片描述

序列号部分的不连续性

在雪花算法中,排在最后的12位自增序列号部分,默认的生成逻辑是当时间戳部分相等时,自增序列号部分才会+1,否则,将从0重新开始。我们想想这样的话会有什么问题,因为时间戳相同的情况很少,所以我们生成出来的id末尾大部分会导致取模的时候分布并不均匀,比如分库分表时,数据大部分就会落到一个地方,不适用于需要做取模运算的场景。

我们先复现一下问题,使用hutool的雪花算法工具类生成唯一id,然后做一个简单的取模运算:

    @Test
    public void hutoolSnowflakeMod() throws InterruptedException {
        for (int i = 0; i < 100; i++) {
            long id = IdUtil.getSnowflake(1).nextId();
            Thread.sleep(1);
            log.info("id: {}, after mod 4: {}", id, id % 4);
        }
    }

截取的结果可以看到,基本上就是0,几乎没有其它数字,取模的结果很不均匀。

[2024-08-19 15:46:45.486] [Test worker] [ INFO] [o.OakHybridCacheWithDBCosIdTest.hutoolSnowflakeMod(41)] - id: 1825439244152344576, after mod 4: 0
[2024-08-19 15:46:45.487] [Test worker] [ INFO] [o.OakHybridCacheWithDBCosIdTest.hutoolSnowflakeMod(41)] - id: 1825439244160733184, after mod 4: 0
[2024-08-19 15:46:45.490] [Test worker] [ INFO] [o.OakHybridCacheWithDBCosIdTest.hutoolSnowflakeMod(41)] - id: 1825439244164927488, after mod 4: 0
[2024-08-19 15:46:45.492] [Test worker] [ INFO] [o.OakHybridCacheWithDBCosIdTest.hutoolSnowflakeMod(41)] - id: 1825439244177510400, after mod 4: 0
[2024-08-19 15:46:45.493] [Test worker] [ INFO] [o.OakHybridCacheWithDBCosIdTest.hutoolSnowflakeMod(41)] - id: 1825439244185899008, after mod 4: 0
[2024-08-19 15:46:45.496] [Test worker] [ INFO] [o.OakHybridCacheWithDBCosIdTest.hutoolSnowflakeMod(41)] - id: 1825439244190093312, after mod 4: 0
[2024-08-19 15:46:45.498] [Test worker] [ INFO] [o.OakHybridCacheWithDBCosIdTest.hutoolSnowflakeMod(41)] - id: 1825439244202676224, after mod 4: 0
[2024-08-19 15:46:45.501] [Test worker] [ INFO] [o.OakHybridCacheWithDBCosIdTest.hutoolSnowflakeMod(41)] - id: 1825439244211064832, after mod 4: 0
[2024-08-19 15:46:45.503] [Test worker] [ INFO] [o.OakHybridCacheWithDBCosIdTest.hutoolSnowflakeMod(41)] - id: 1825439244223647744, after mod 4: 0
[2024-08-19 15:46:45.505] [Test worker] [ INFO] [o.OakHybridCacheWithDBCosIdTest.hutoolSnowflakeMod(41)] - id: 1825439244232036352, after mod 4: 0
[2024-08-19 15:46:45.507] [Test worker] [ INFO] [o.OakHybridCacheWithDBCosIdTest.hutoolSnowflakeMod(41)] - id: 1825439244240424960, after mod 4: 0

在CosId框架中,解决方案也很简单 – 轻易不要重置这个自增序列位即可,通过引入 sequenceResetThreshold 属性,巧妙地解决了取模分片不均匀的问题,这一设计在无需牺牲性能的同时,为用户提供了更加出色的使用体验。

sequenceResetThreshold 在不同的情况下可能会取不同的值,但是作用都是一样的,通过限制自增序列不要轻易重置来达到目的。

AbstractSnowflakeId

//region Reset sequence based on sequence reset threshold,Optimize the problem of uneven sharding.

if (currentTimestamp > lastTimestamp
   && sequence >= sequenceResetThreshold) {
   sequence = 0L;
}

我们跑一遍CosId的取模情况:

    @Test
    public void cosIdSnowflakeMod() throws InterruptedException {
        for (int i = 0; i < 100; i++) {
            long id = snowflakeId.generate();
            Thread.sleep(1);
            log.info("id: {}, after mod 4: {}", id, id % 4);
        }
    }

可以看出已经不存在取模分配不均匀的问题

[2024-08-19 15:50:35.949] [Test worker] [ INFO] [o.OakHybridCacheWithDBCosIdTest.cosIdSnowflakeMod(50)] - id: 615936209755045889, after mod 4: 1
[2024-08-19 15:50:35.951] [Test worker] [ INFO] [o.OakHybridCacheWithDBCosIdTest.cosIdSnowflakeMod(50)] - id: 615936209763434498, after mod 4: 2
[2024-08-19 15:50:35.953] [Test worker] [ INFO] [o.OakHybridCacheWithDBCosIdTest.cosIdSnowflakeMod(50)] - id: 615936209771823107, after mod 4: 3
[2024-08-19 15:50:35.955] [Test worker] [ INFO] [o.OakHybridCacheWithDBCosIdTest.cosIdSnowflakeMod(50)] - id: 615936209780211716, after mod 4: 0
[2024-08-19 15:50:35.957] [Test worker] [ INFO] [o.OakHybridCacheWithDBCosIdTest.cosIdSnowflakeMod(50)] - id: 615936209788600325, after mod 4: 1
[2024-08-19 15:50:35.959] [Test worker] [ INFO] [o.OakHybridCacheWithDBCosIdTest.cosIdSnowflakeMod(50)] - id: 615936209796988934, after mod 4: 2
[2024-08-19 15:50:35.961] [Test worker] [ INFO] [o.OakHybridCacheWithDBCosIdTest.cosIdSnowflakeMod(50)] - id: 615936209805377543, after mod 4: 3
[2024-08-19 15:50:35.963] [Test worker] [ INFO] [o.OakHybridCacheWithDBCosIdTest.cosIdSnowflakeMod(50)] - id: 615936209813766152, after mod 4: 0

JavaScript数值溢出

JavaScriptNumber.MAX_SAFE_INTEGER只有53-bit,如果直接将63位的SnowflakeId返回给前端,那么会产生值溢出的情况(所以这里我们应该知道后端传给前端的long值溢出问题,迟早会出现,只不过SnowflakeId出现得更快而已)。 很显然溢出是不能被接受的,一般可以使用以下俩种处理方案:

  • 将生成的63-bitSnowflakeId转换为String类型。
    • 直接将long转换成String
    • (CosId方案)使用SnowflakeFriendlyIdSnowflakeId转换成比较友好的字符串表示:{timestamp}-{machineId}-{sequence} -> 20210623131730192-1-0
  • 自定义SnowflakeId位分配来缩短SnowflakeId的位数(53-bit)使 ID 提供给前端时不溢出
    • (CosId方案)使用SafeJavaScriptSnowflakeId(JavaScript 安全的 SnowflakeId)

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

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

相关文章

微分方程(Blanchard Differential Equations 4th)中文版Section1.6

平衡点与相直线 给定一个微分方程 d y d t = f ( t , y ) , \frac{dy}{dt} = f(t, y), dtdy​=f(t,y), 我们可以通过绘制斜率场和勾勒图形来大致了解解的行为,或者使用欧拉法计算近似解。有时我们甚至可以推导出解的显式公式并绘制结果。所有这些技术都需要相当多的工作,无…

武汉流星汇聚:西班牙时尚消费高涨,中国商家借亚马逊平台拓商机

在2024年第二季度的亚马逊西班牙站&#xff0c;一场前所未有的时尚盛宴正悄然上演。销售额同比高增长TOP10品类榜单的揭晓&#xff0c;不仅揭示了西班牙消费者对于时尚品类的狂热追求&#xff0c;更为亚马逊平台上的中国商家开启了一扇通往新蓝海的大门。其中&#xff0c;男士拳…

使用LlamaIndex中的Reli 进行实体链接和关系提取

从文本中构建知识图谱一直是一个引人入胜的研究领域。随着大型语言模型(LLM)的出现,这一领域获得了更多主流关注。然而,大型语言模型的成本可能相当高昂。另一种方法是对较小的模型进行微调,这种方法得到了学术研究的支持,并产生了更有效的解决方案。今天,我们将探讨罗马…

redis mysql oracle mssql postgresql提权工具mdut

mdut工具使用 mdut用于数据库的连接&#xff0c;连接成功后可用户反弹shell&#xff0c;命令执行 mdut工具运行说明 1&#xff0c;此工具需要在jdk1.8的环境下运行 2&#xff0c;下载完工具包之后&#xff0c;找到java1.8环境&#xff0c;运行jar文件 java.exe -jar Multipl…

Linux Redis 删除指定库下所有 Key

代码示例 以下是每一步需要执行的代码及其注释&#xff1a; 连接 Redis redis-cli -h <hostname> -p <port> -a <password>-h&#xff1a;指定 Redis 服务器的主机名。 -p&#xff1a;指定 Redis 服务器的端口号。 -a&#xff1a;指定 Redis 服务器的密码。…

基于Arch的轻量级发行版Archcraft结合内网穿透实现远程SSH连接

文章目录 前言1. 本地SSH连接测试2. Archcraft安装Cpolar3. 配置 SSH公网地址4. 公网远程SSH连接5. 固定SSH公网地址6. SSH固定地址连接 前言 本文主要介绍如何在Archcraft系统中安装Cpolar内网穿透工具,并以实现Windows环境ssh远程连接本地局域网Archcraft系统来说明使用内网…

ubuntu安装虚拟环境(tensorflow、torch)

一、安装需求 1、确保ubuntu可以ping通百度 2、设置好了pip镜像源&#xff0c;&#xff08;具体可看&#xff1a;ubuntu配pip的源-CSDN博客&#xff09; 二、安装虚拟环境&#xff08;务必使用sudo进行&#xff09; step1&#xff1a;执行安装命令 更改了pip默认使用pip3的…

SpringBoot+Vue在线商城(电子商城)系统-附源码与配套论文

摘 要 随着互联网技术的发展和普及&#xff0c;电子商务在全球范围内得到了迅猛的发展&#xff0c;已经成为了一种重要的商业模式和生活方式。电子商城是电子商务的重要组成部分&#xff0c;是一个基于互联网的商业模式和交易平台&#xff0c;通过网络进行产品和服务的销售。…

18705 01背包问题

### 分析 这是一个典型的0/1背包问题。我们需要在有限的背包容量下&#xff0c;选择若干物品&#xff0c;使得获得的总价值最大。可以使用动态规划来解决这个问题。 ### 伪代码 1. 定义一个一维数组dp&#xff0c;其中dp[j]表示容量为j的背包能获得的最大价值。 2. 初始化dp[0…

STM32的相关简单介绍

一、什么是STM32 STM32是ST公司设计的一系列以ARM Cortex-M为核心的32位微控制器 ST公司&#xff0c;即意法半导体集团(STMicrolectronics,简称ST)&#xff0c;1987年成立。由意大利的SGS微电子公司和法国Thomson半导体公司合并而成。 在当下的32位微控制器中&#xff0c;STM…

系统主机加固的十个方法,教你做好主机加固

环境背景 随着全球数字化转型的加速&#xff0c;企业IT环境变得愈发复杂&#xff0c;服务器主机面临的安全威胁也日益多样化。无论是工业控制系统、企业内部网络、企业内部服务器&#xff0c;还是云计算环境&#xff0c;都可能成为网络攻击的目标。此外&#xff0c;随着“工业…

重构版:链动3+1创新裂变模式解析

链动31模式&#xff0c;作为一种创新的市场扩张策略&#xff0c;专注于通过产品的独特魅力驱动用户自主传播与裂变。与传统的链动21模式相比&#xff0c;它在结构上进行了重大革新&#xff0c;不再局限于传统的太阳线裂变方式&#xff0c;而是引入了四四复制的架构&#xff0c;…

【Python零基础】while循环和用户输入

文章目录 前言一、input()函数二、while循环三、使用while循环来处理列表和字典总结 前言 我们开发一个应用程序&#xff0c;目的都是为了解决最终用户的问题&#xff0c;针对用户界面输入的数据&#xff0c;按照用户期待的逻辑进行处理&#xff0c;得到用户想要的结果。本章将…

如何查看Squid的DNS缓存

使用squidclient mgr:ipcache命令查看Squid的DNS缓存记录 如果squid端口不是3128, 需要指定端口号, squidclient -p {port} mgr:ipcache # squidclient mgr:ipcache ... IP Cache Statistics: ... IP Cache Contents:Hostname Flg lstref TTL N(b)…

【排序算法】八大排序(上)(c语言实现)(附源码)

&#x1f31f;&#x1f31f;作者主页&#xff1a;ephemerals__ &#x1f31f;&#x1f31f;所属专栏&#xff1a;算法 目录 前言 写一串测试数据 交换两元素的函数 一、冒泡排序 二、选择排序 三、插入排序 四、希尔排序 程序全部代码 总结 前言 排序算法是计算机科…

【pwnable.kr】0x01-fd Writeup

题目描述 解法 Ubuntu连接靶机&#xff08;连不通的可以试一下proxychains&#xff09; ssh fdpwnable.kr -p2222scp命令拷贝下fd源码文件 scp -P2222 fdpwnable.kr:fd.c .查看源码 #include <stdio.h> #include <stdlib.h> #include <string.h> char bu…

解决k8s分布式集群,子节点加入到主节点失败的问题

1.问题情况 Master主节点在 使用 kubeadm init 成功进行初始化后&#xff0c;如下所示 Your Kubernetes control-plane has initialized successfully!To start using your cluster, you need to run the following as a regular user:mkdir -p $HOME/.kubesudo cp -i /etc/k…

spring全面详解-最全最详细的spring基本认识和入门使用

文章目录 Springspring概述1 Spring定义2 Spring核心3 Spring Framework的特点 入门案例1 环境要求2 构建工程2.1 构建子工程first-spring2.2 入门案例2.3 对象存储 IoC容器1 控制反转IoC2 依赖注入DI3 IoC容器实现4 基于XML管理bean4.1 环境准备4.2 获取bean方式4.3 基于sette…

【微信小程序】自定义组件 - behaviors

1. 什么是 behaviors 2. behaviors 的工作方式 3. 创建 behavior 调用 Behavior(Object object) 方法即可创建一个共享的 behavior 实例对象&#xff0c;供所有的组件使用&#xff1a; 4. 导入并使用 behavior 5. behavior 中所有可用的节点 6. 同名字段的覆盖和组合规则* 关…

C++学习笔记----4、用C++进行程序设计(一)---- 什么是面向对象的程序设计

也许你看到这个题目的时候&#xff0c;就觉得这篇博文不用看了&#xff0c;难道这就是题目劝退了观众。我看到过一些程序&#xff0c;是由面向过程的传统程序修改过来了&#xff0c;只是将原来的函数变成了类的成员函数&#xff0c;其他几乎没有什么变化&#xff0c;可以说是换…