一、前言


在日趋复杂的分布式系统中,数据量越来越大,数据库分库分表是一贯的垂直水平做法,但是需要一个全局唯一ID标识一条数据或者MQ消息,数据库id自增就显然不能满足要求了。因为场景不同,分布式ID需要满足以下几个条件:

全局唯一性,不能出现重复的ID。
趋势递增,在MySQL InnoDB引擎中使用的是聚集索引,由于多数RDBMS使用B-tree的数据结构来存储索引数据,在主键的选择上应该尽量使用有序的主键保证写入性能。
单调递增,保证下一个ID一定大于上一个ID。例如分布式事务版本号、IM增量消息、排序等特殊需求。
信息安全,对于特殊业务,如订单等,分布式ID生成应该是无规则的,不能从ID上反解析出流量等敏感信息。
市面上对分布式ID生成大致有几种算法(一些开源项目都是围着这几种算法进行实现和优化):

UUID:因为是本地生成,性能极高,但是生成的ID太长,16字节128位,通常需要字符串类型存储,且无序,所以很多场景不适用,也不适用于作为MySQL数据库的主键和索引(MySql官方建议,主键越短越好;对于InnoDB引擎,索引的无序性可能会引起数据位置频繁变动,严重影响性能)。
数据库自增ID:每次获取ID都需要DB的IO操作,DB压力大,性能低。数据库宕机对外依赖服务就是毁灭性打击,不过可以部署数据库集群保证高可用。
数据库号段算法:对数据库自增ID的优化,每次获取一个号段的值。用完之后再去数据库获取新的号段,可以大大减轻数据库的压力。号段越长,性能越高,同时如果数据库宕机,号段没有用完,短时间还可以对外提供服务。(美团的Leaf、滴滴的TinyId)
雪花算法:Twitter开源的snowflake,以时间戳+机器+递增序列组成,基本趋势递增,且性能很高,因为强依赖机器时钟,所以需要考虑时钟回拨问题,即机器上的时间可能因为校正出现倒退,导致生成的ID重复。(百度的uid-generator、美团的Leaf)
雪花算法和数据库号段算法用的最多,本篇主要对雪花算法原理剖析和解决时钟回拨问题讨论。

二、雪花算法snowflake

1、基本定义


snowflake原理其实很简单,生成一个64bit(long)的全局唯一ID,标准元素以1bit无用符号位+41bit时间戳+10bit机器ID+12bit序列化组成,其中除1bit符号位不可调整外,其他三个标识的bit都可以根据实际情况调整:

41bit-时间可以表示(1L<<41)/(1000L360024*365)=69年的时间。
10bit-机器可以表示1024台机器。如果对IDC划分有需求,还可以将10-bit分5-bit给IDC,分5-bit给工作机器。这样就可以表示32个IDC,每个IDC下可以有32台机器。
12个自增序列号可以表示2^12个ID,理论上snowflake方案的QPS约为409.6w/s。
注:都是从0开始计数。

2、snowflake的优缺点
优点:

毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。
可以不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也非常高。
可以根据自身业务特性分配bit位,非常灵活。
缺点:

强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务处于不可用状态。

三、Java代码实现snowflake

public class SnowflakeIdGenerator {public static final int TOTAL_BITS = 1 << 6;private static final long SIGN_BITS = 1;private static final long TIME_STAMP_BITS = 41L;private static final long DATA_CENTER_ID_BITS = 5L;private static final long WORKER_ID_BITS = 5L;private static final long SEQUENCE_BITS = 12L;/*** 时间向左位移位数 22位*/private static final long TIMESTAMP_LEFT_SHIFT = WORKER_ID_BITS + DATA_CENTER_ID_BITS + SEQUENCE_BITS;/*** IDC向左位移位数 17位*/private static final long DATA_CENTER_ID_SHIFT = WORKER_ID_BITS + SEQUENCE_BITS;/*** 机器ID 向左位移位数 12位*/private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;/*** 序列掩码,用于限定序列最大值为4095*/private static final long SEQUENCE_MASK =  -1L ^ (-1L << SEQUENCE_BITS);/*** 最大支持机器节点数0~31,一共32个*/private static final long MAX_WORKER_ID = -1L ^ (-1L << WORKER_ID_BITS);/*** 最大支持数据中心节点数0~31,一共32个*/private static final long MAX_DATA_CENTER_ID = -1L ^ (-1L << DATA_CENTER_ID_BITS);/*** 最大时间戳 2199023255551*/private static final long MAX_DELTA_TIMESTAMP = -1L ^ (-1L << TIME_STAMP_BITS);/*** Customer epoch*/private final long twepoch;private final long workerId;private final long dataCenterId;private long sequence = 0L;private long lastTimestamp = -1L;/**** @param workerId 机器ID* @param dataCenterId  IDC ID*/public SnowflakeIdGenerator(long workerId, long dataCenterId) {this(workerId, dataCenterId, null);}/**** @param workerId  机器ID* @param dataCenterId IDC ID* @param epochDate 初始化时间起点*/public SnowflakeIdGenerator(long workerId, long dataCenterId, Date epochDate) {if (workerId > MAX_WORKER_ID || workerId < 0) {throw new IllegalArgumentException("worker Id can't be greater than "+ MAX_WORKER_ID + " or less than 0");}if (dataCenterId > MAX_DATA_CENTER_ID || dataCenterId < 0) {throw new IllegalArgumentException("datacenter Id can't be greater than {" + MAX_DATA_CENTER_ID + "} or less than 0");}this.workerId = workerId;this.dataCenterId = dataCenterId;if (epochDate != null) {this.twepoch = epochDate.getTime();} else {//2010-10-11this.twepoch = 1286726400000L;}}public long genID() throws Exception {try {return nextId();} catch (Exception e) {throw e;}}public long getLastTimestamp() {return lastTimestamp;}/*** 通过移位解析出sequence,sequence有效位为[0,12]* 所以先向左移64-12,然后再像右移64-12,通过两次移位就可以把无效位移除了* @param id* @return*/public long getSequence2(long id) {return (id << (TOTAL_BITS - SEQUENCE_BITS)) >>> (TOTAL_BITS - SEQUENCE_BITS);}/*** 通过移位解析出workerId,workerId有效位为[13,17], 左右两边都有无效位* 先向左移 41+5+1,移除掉41bit-时间,5bit-IDC、1bit-sign,* 然后右移回去41+5+1+12,从而移除掉12bit-序列号* @param id* @return*/public long getWorkerId2(long id) {return (id << (TIME_STAMP_BITS + DATA_CENTER_ID_BITS + SIGN_BITS)) >>> (TIME_STAMP_BITS + DATA_CENTER_ID_BITS + SEQUENCE_BITS + SIGN_BITS);}/*** 通过移位解析出IDC_ID,dataCenterId有效位为[18,23],左边两边都有无效位* 先左移41+1,移除掉41bit-时间和1bit-sign* 然后右移回去41+1+5+12,移除掉右边的5bit-workerId和12bit-序列号* @param id* @return*/public long getDataCenterId2(long id) {return (id << (TIME_STAMP_BITS + SIGN_BITS)) >>> (TIME_STAMP_BITS + WORKER_ID_BITS + SEQUENCE_BITS + SIGN_BITS);}/*** 41bit-时间,左边1bit-sign为0,可以忽略,不用左移,所以只需要右移,并加上起始时间twepoch即可。* @param id* @return*/public long getGenerateDateTime2(long id) {return (id >>> (DATA_CENTER_ID_BITS + WORKER_ID_BITS + SEQUENCE_BITS)) + twepoch;}public long getSequence(long id) {return id & ~(-1L << SEQUENCE_BITS);}public long getWorkerId(long id) {return id >> WORKER_ID_SHIFT & ~(-1L << WORKER_ID_BITS);}public long getDataCenterId(long id) {return id >> DATA_CENTER_ID_SHIFT & ~(-1L << DATA_CENTER_ID_BITS);}public long getGenerateDateTime(long id) {return (id >> TIMESTAMP_LEFT_SHIFT & ~(-1L << 41L)) + twepoch;}private synchronized long nextId() throws Exception {long timestamp = timeGen();// 1、出现时钟回拨问题,直接抛异常if (timestamp < lastTimestamp) {long refusedTimes = lastTimestamp - timestamp;// 可自定义异常类throw new UnsupportedOperationException(String.format("Clock moved backwards. Refusing for %d seconds", refusedTimes));}// 2、时间等于lastTimestamp,取当前的sequence + 1if (timestamp == lastTimestamp) {sequence = (sequence + 1) & SEQUENCE_MASK;// Exceed the max sequence, we wait the next second to generate idif (sequence == 0) {timestamp = tilNextMillis(lastTimestamp);}} else {// 3、时间大于lastTimestamp没有发生回拨, sequence 从0开始this.sequence = 0L;}lastTimestamp = timestamp;return allocate(timestamp - this.twepoch);}private long allocate(long deltaSeconds) {return (deltaSeconds << TIMESTAMP_LEFT_SHIFT) | (this.dataCenterId << DATA_CENTER_ID_SHIFT) | (this.workerId << WORKER_ID_SHIFT) | this.sequence;}private long timeGen() {long currentTimestamp = System.currentTimeMillis();// 时间戳超出最大值if (currentTimestamp - twepoch > MAX_DELTA_TIMESTAMP) {throw new UnsupportedOperationException("Timestamp bits is exhausted. Refusing ID generate. Now: " + currentTimestamp);}return currentTimestamp;}private long tilNextMillis(long lastTimestamp) {long timestamp = timeGen();while (timestamp <= lastTimestamp) {timestamp = timeGen();}return timestamp;}/*** 测试* @param args*/public static void main(String[] args) throws Exception {SnowflakeIdGenerator snowflakeIdGenerator = new SnowflakeIdGenerator(1,2);long id = snowflakeIdGenerator.genID();System.out.println("ID=" + id + ", lastTimestamp=" + snowflakeIdGenerator.getLastTimestamp());System.out.println("ID二进制:" + Long.toBinaryString(id));System.out.println("解析ID:");System.out.println("Sequence=" + snowflakeIdGenerator.getSequence(id));System.out.println("WorkerId=" + snowflakeIdGenerator.getWorkerId(id));System.out.println("DataCenterId=" + snowflakeIdGenerator.getDataCenterId(id));System.out.println("GenerateDateTime=" + snowflakeIdGenerator.getGenerateDateTime(id));System.out.println("Sequence2=" + snowflakeIdGenerator.getSequence2(id));System.out.println("WorkerId2=" + snowflakeIdGenerator.getWorkerId2(id));System.out.println("DataCenterId2=" + snowflakeIdGenerator.getDataCenterId2(id));System.out.println("GenerateDateTime2=" + snowflakeIdGenerator.getGenerateDateTime2(id));}}

四、时钟回拨问题和解决方案讨论

1、时间戳自增彻底解决时钟回拨问题

private long sequence = -1L;
private long startTimestamp = 1623947387000L;
private synchronized  long nextId2() {long sequenceTmp = sequence;sequence = (sequence + 1) & SEQUENCE_MASK;// sequence =0 有可能是初始+1=0,也可能是超过了最大值等于0// 所以把 初始+1=0排除掉if (sequence == 0 && sequenceTmp >= 0) {// sequence自增到最大了,时间戳自增1startTimestamp += 1;}// 生成idreturn allocate(startTimestamp - twepoch);
}

2、缓存历史序列号缓解时钟回拨问题

// 记录近2S的毫秒数的sequence的缓存
private int LENGTH = 2000;
// sequence缓存
private long[] sequenceCycle = new long[LENGTH];private synchronized long nextId() throws Exception {long timestamp = timeGen();int index = (int)(timestamp % LENGTH);// 1、出现时钟回拨问题,获取历史序列号自增if (timestamp < lastTimestamp) {long sequence = 0;do {if ((lastTimestamp - timestamp) > LENGTH) {// 可自定义异常、告警等,短暂不能对外提供,故障转移,将请求转发到正常机器。throw new UnsupportedOperationException("The timeback range is too large and exceeds 2000ms caches");}long preSequence = sequenceCycle[index];sequence = (preSequence + 1) & SEQUENCE_MASK;if (sequence == 0) {// 如果取出的历史序列号+1后已经达到超过最大值,// 则重新获取timestamp,重新拿其他位置的缓存timestamp = tilNextMillis(lastTimestamp);index = (int)(timestamp % LENGTH);} else {// 更新缓存sequenceCycle[index] = this.sequence;            return allocate((timestamp - this.twepoch), sequence);}} while (timestamp < lastTimestamp);// 如果在获取缓存的过程中timestamp恢复正常了,就走正常流程}// 2、时间等于lastTimestamp,取当前的sequence + 1if (timestamp == lastTimestamp) {sequence = (sequence + 1) & SEQUENCE_MASK;// Exceed the max sequence, we wait the next second to generate idif (sequence == 0) {timestamp = tilNextMillis(lastTimestamp);index = (int)(timestamp % LENGTH);}} else {// 3、时间大于lastTimestamp没有发生回拨, sequence 从0开始this.sequence = 0L;}// 缓存sequence + 更新lastTimestampsequenceCycle[index] = this.sequence;lastTimestamp = timestamp;// 生成idreturn allocate(timestamp - this.twepoch);
}

3、等待时钟校正

private synchronized  long nextId3() {long timestamp = timeGen();// 1、出现时钟回拨问题,如果回拨幅度不大,等待时钟自己校正if (timestamp < lastTimestamp) {int sleepCntMax = 2;int sleepCnt = 0;do {long sleepTime = lastTimestamp - timestamp;if (sleepCnt > sleepCntMax) {// 可自定义异常类throw new UnsupportedOperationException(String.format("Clock moved backwards. Refusing for %d seconds", sleepTime));}if (sleepTime <= 500) {try {Thread.sleep(sleepTime);} catch (InterruptedException e) {e.printStackTrace();} finally {sleepCnt++;timestamp = tilNextMillis(lastTimestamp);}} else {// 可自定义异常类throw new UnsupportedOperationException(String.format("Clock moved backwards. Refusing for %d seconds", sleepTime));}} while (timestamp < lastTimestamp);}// 2、时间等于lastTimestamp,取当前的sequence + 1if (timestamp == lastTimestamp) {sequence = (sequence + 1) & SEQUENCE_MASK;// Exceed the max sequence, we wait the next second to generate idif (sequence == 0) {timestamp = tilNextMillis(lastTimestamp);}} else {// 3、时间大于lastTimestamp没有发生回拨, sequence 从0开始this.sequence = 0L;}lastTimestamp = timestamp;// 生成idreturn allocate(timestamp - this.twepoch);
}

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

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【PCB设计经验】去耦电容如何布局?

0805 和 0603 以及更小 封装的电容用作于对中高频的去耦,其摆放位置是有要求的: 一、建议尽可能的靠近主控芯片的 电源管脚放置。 二、使用较宽和短的引线连接到电源和地过孔可以采用如下 图 4–1 中的图 ( 2 )、( 3)、 ( 4 )任意一种方式,避免使用长线或者较细的…

自动化运维实验

目录 一、实验拓扑 二、实验目的 三、实验步骤 实验思路&#xff1a; 代码部分&#xff1a; 四、实验结果&#xff1a; 一、实验拓扑 二、实验目的 利用python脚本&#xff0c;在本地&#xff0c;或者虚拟机里实现&#xff0c;设备CRC数量统计&#xff0c;并输出成表格 三、实验…

Wed前端第二次作业

一、作业1&#xff1a;完成自己学校的官网&#xff0c;动忘内容直接贴&#xff0c;至少三个不同的页面1、界面1&#xff08;1&#xff09;相关代码<!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name&quo…

第5节 大模型分布式推理通信优化与硬件协同

前言 在分布式推理中,多设备(如GPU、CPU)之间的数据传输(通信)是连接计算的“桥梁”。如果通信效率低下,即使单设备计算能力再强,整体性能也会大打折扣。想象一下:如果工厂之间的物流卡车跑得比生产速度还慢,再多的工厂也无法提高整体产量。 本节将从最基础的单设备内…

XGBoost 的适用场景以及与 CNN、LSTM 的区别

XGBoost 的核心优势与适用场景XGBoost 是一种梯度提升决策树算法&#xff0c;属于集成学习方法。它在处理结构化/表格化数据方面表现极其出色&#xff0c;是 Kaggle 竞赛和工业界广泛应用的“冠军”模型。其核心优势和应用场景包括&#xff1a;1. 结构化/表格化数据数据形式&a…

快速设计简单嵌入式操作系统(3):动手实操,基于STC8编写单任务执行程序,感悟MCU指令的执行过程

引言 前面我们陆续学习了操作系统常见的基础概念&#xff0c;接着简单了解了一下8051单片机的内存结构和执行顺序切换的相关概念。接下来&#xff0c;我们就开始进行实操&#xff0c;基于8051单片机STC8来编写一个简单的操作系统&#xff0c;这里我们先实现一个单任务的执行程…

Spring AI Alibaba - 聊天机器人快速上手

本节对应 Github&#xff1a;https://github.com/JCodeNest/JCodeNest-AI-Alibaba/tree/master/spring-ai-alibaba-helloworld 本文将以阿里巴巴的通义大模型为例&#xff0c;通过 Spring AI Alibaba 组件&#xff0c;手把手带你完成从零到一的构建过程&#xff1a;首先&#…

串口通信学习

不需要校验位就选8位&#xff0c;需要校验位就选9位&#xff01;USRTUSART框图STM32的外设引脚这是USART的基本结构。数据帧&#xff0c;八位是这个公式还是很重要的&#xff01;如果在编辑器里面使用printf打印汉字的话&#xff0c;会出现乱码的话&#xff0c;前提是你的编码格…

面试经典150题[001]:合并两个有序数组(LeetCode 88)

合并两个有序数组&#xff08;LeetCode 88&#xff09; https://leetcode.cn/problems/merge-sorted-array/?envTypestudy-plan-v2&envIdtop-interview-150 1. 题目背景 你有两个已经排好序的数组&#xff1a; nums1&#xff1a;前面是有效数字&#xff0c;后面是空位&…

快速安装达梦8测试库

计划&#xff1a;数据库名实例名PORT_NUMMAL_INST_DW_PORTMAL_HOSTMAL_PORTMAL_DW_PORTDMDWDBINST_1533615101192.168.207.612510135101*****[2025-08-11 15:14:34]***** Last login: Fri Jul 25 17:36:04 2025 from 192.168.88.48 [rootdm01 ~]# ip a 1: lo: <LOOPBACK,UP,…

Hive中优化问题

一、小文件合并优化Hive中的小文件分为Map端的小文件和Reduce端的小文件。(1)、Map端的小文件优化是通过CombineHiveInputFormat操作。相关的参数是&#xff1a;set hive.input.formatorg.apache.hadoop.hive.ql.io.CombineHiveInputFormat;(2)、Reduce端的小文件合并Map端的小…

tlias智能学习辅助系统--Maven高级-继承

目录 一、打包方式与应用场景 二、父子工程继承关系 1. 父工程配置 2. 子工程配置 三、自定义属性与引用属性 1. 定义属性 2. 在 dependencyManagement 中引用 3. 子工程中引用 四、dependencyManagement 与 dependencies 的区别 五、项目结构示例 六、小结 在实际开…

把 AI 押进“小黑屋”——基于 LLM 的隐私对话沙盒设计与落地

标签&#xff1a;隐私计算、可信执行环境、LLM、沙盒、内存加密、TEE、SGX、Gramine ---- 1. 背景&#xff1a;甲方爸爸一句话&#xff0c;“数据不能出机房” 我们给某三甲医院做智能问诊助手&#xff0c;模型 70 B、知识库 300 GB。 甲方只给了两条铁律&#xff1a; 1. 患者…

Java 大视界 -- Java 大数据在智能教育学习效果评估指标体系构建与精准评估中的应用(394)

Java 大视界 -- Java 大数据在智能教育学习效果评估指标体系构建与精准评估中的应用&#xff08;394&#xff09;引言&#xff1a;正文&#xff1a;一、传统学习评估的 “数字陷阱”&#xff1a;看不全、说不清、跟不上1.1 评估维度的 “单行道”1.1.1 分数掩盖的 “学习真相”…

Dubbo 3.x源码(33)—Dubbo Consumer接收服务调用响应

基于Dubbo 3.1&#xff0c;详细介绍了Dubbo Consumer接收服务调用响应 此前我们学习了Dubbo Provider处理服务调用请求的流程&#xff0c;现在我们来学习Dubbo Consumer接收服务调用响应流程。 实际上接收请求和接收响应同属于接收消息&#xff0c;它们的流程的很多步骤是一样…

栈和队列:数据结构中的基础与应用​

栈和队列&#xff1a;数据结构中的基础与应用在计算机科学的领域中&#xff0c;数据结构犹如大厦的基石&#xff0c;支撑着各类复杂软件系统的构建。而栈和队列作为两种基础且重要的数据结构&#xff0c;以其独特的特性和广泛的应用&#xff0c;在程序设计的舞台上扮演着不可或…

服务端配置 CORS解决跨域问题的原理

服务端配置 CORS&#xff08;跨域资源共享&#xff09;的原理本质是 浏览器与服务器之间的安全协商机制。其核心在于服务器通过特定的 HTTP 响应头声明允许哪些外部源&#xff08;Origin&#xff09;访问资源&#xff0c;浏览器根据这些响应头决定是否放行跨域请求。以下是详细…

Unity笔记(五)知识补充——场景切换、退出游戏、鼠标隐藏锁定、随机数、委托

写在前面&#xff1a;写本系列(自用)的目的是回顾已经学过的知识、记录新学习的知识或是记录心得理解&#xff0c;方便自己以后快速复习&#xff0c;减少遗忘。主要是C#代码部分。十七、场景切换和退出游戏1、场景切换场景切换使用方法&#xff1a; SceneManager.LoadScene()&a…

用 Spring 思维快速上手 DDD——以 Kratos 为例的分层解读

用 Spring 思维理解 DDD —— 以 Kratos 为参照 ​ 在此前的学习工作中&#xff0c;使用的开发框架一直都是 SpringBoot&#xff0c;对 MVC 架构几乎是肌肉记忆&#xff1a;Controller 接请求&#xff0c;Service 写业务逻辑&#xff0c;Mapper 操作数据库&#xff0c;这套套路…

docspace|Linux|使用docker完全离线化部署onlyoffice之docspace文档协作系统(全网首发)

一、 前言 书接上回&#xff0c;Linux|实用工具|onlyoffice workspace使用docker快速部署&#xff08;离线和定制化部署&#xff09;-CSDN博客&#xff0c;如果是小公司或者比如某个项目组内部使用&#xff0c;那么&#xff0c;使用docspace这个文档协同系统是非常合适的&…