漫谈分布式唯一ID

文章目录

  • 本系列
  • 前言
  • UUID
  • DB自增主键
  • Redis incr命令
  • 号段模式
  • 雪花算法

本系列

  • 漫谈分布式唯一ID(本文)
  • 分布式唯一ID生成(二):leaf
  • 分布式唯一ID生成(三):uid-generator
  • 分布式唯一ID生成(四):tinyid

前言

在大多数业务场景中,需要对每条数据分配一个唯一ID作为标识。大部分关系型数据库提供了自增主键功能来支持该需求

但若数据量较大,需要分库分表时,就不能使用每个数据库实例提供的自增键功能,因为不能保证在所有表中唯一,分布式全局唯一ID的需求应运而生

分布式唯一ID有以下功能需求:

  1. 全局唯一性:在某个业务场景下唯一,避免数据冲突,这是最基本的要求
  2. 高性能:生成速度快,不能阻塞业务流程
  3. 趋势递增:通常会将该ID作为数据库主键,由于mysql innoDB采用聚集索引,若新增的记录主键无序,可能造成 叶分裂空间利用率不高 的问题,降低写入性能
  4. 严格单调递增:适用于需要这种特性的场景,例如IM场景中需要根据此生成消息ID,用于给消息排序,并判断是否有消息丢失
  5. 安全性:防止泄露业务信息,若生成的ID严格递增,在电商场景可根据一段时间的id差算出订单量
  6. 方便追踪:例如在ID融入时间戳,就能知道是什么时间范围生成的

上述4,5需求是互斥的,无法同时满足。不同业务场景根据需求选择

还有一些非功能需求:

  1. 高可用:可用性至少5个9

  2. 低延迟:生成速度一定要快,TP50和TP99.9都要非常快,不能因为这个导致业务接口响应变慢

  3. 高并发:假如一下来10w个生成分布式 ID 的请求,要能扛得住

接下来介绍一些常见的分布式ID生成方案


UUID

UUID(Universally unique identifier)一般包含32个十六进制的字符,128位,通过一定的算法计算出来,通常基于时间戳,mac地址,随机数等

优点为:

  • 性能好:本地生成,无网络消耗
  • 唯一性:可以认为不会发生冲突

某些版本基于命名空间能保证唯一性,某些版本基于随机数生成,不保证唯一性,但出现相同UUID的概率非常小,根据百度百科的说法,以java.util.UUID为例,每秒产生10亿笔UUID,100年后只产生一次重复的几率是50%

缺点:

  • 没有趋势递增特性:作为数据库主键时插入性能不高,会导致叶分裂
  • 数据较宽:通常UUID为128bit,不能用mysql的bigint存储,需使用字符串类型,对性能有一定影响
  • 信息不安全:基于mac生成的uuid可能造成mac地址泄露

应用:生成traceId或logId


DB自增主键

以mysql为例,我们可以专门建一张表,利用其自增键来生成唯一ID

表结构如下:

CREATE TABLE unique_id (
    id bigint(20) unsigned NOT NULL auto_increment, 
    value char(20) NOT NULL default '',
    PRIMARY KEY (id),
    UNIQUE KEY unique_v(value) 
) ENGINE=MyISAM;

使用以下sql获取id

begin; 
replace into unique_id (value) VALUES ('placeword'); 
select last_insert_id(); 
commit;

这里 使用replace而不是insert ,是为了保证整个表只有一条记录,因为不需要多余的记录也能生成自增id

这种方式利用数据库的自增主键保证生成id的唯一性,严格单调递增。缺点为:

  1. 性能问题:每次生成id需要一次数据库远程IO,发号器的瓶颈取决于db的读写性能
  2. 可用性问题:只用到一台db实例,存在单点问题,可用性没有保障

针对上面两个问题,可以引入多台mysql:每台实例的表使用不同的初始值

以2台mysql实例为例,分别做如下配置:

mysql1:

set @@auto_increment_offset = 1; -- 起始值 

set @@auto_increment_increment = 2; -- 步长

mysql2:

set @@auto_increment_offset = 2; -- 起始值 

set @@auto_increment_increment = 2; -- 步长

mysql1从 1 开始发号,mysql2从 2 开始发号,每次发号后递增2
这样mysql1生成的id序列为:

1,3,5,7,9....

mysql2为:

2,4,6,8....

当请求到来时,采用随机或轮询的方式请求这些实例,这样得到的id序列总体为趋势递增,既减少了单台实例的访问压力,也提高了可用性。缺点为:

  1. 从单调递增变为趋势递增
  2. 性能问题: 每次生成ID还是有一次远程数据库IO,对DB的压力还是大
  3. 伸缩性问题:当需要扩展更多的机器时,需要调整之前所有实例的步长,且需要保证再次期间生成ID不冲突,实现起来较麻烦

Redis incr命令

为了解决数据库自增键遇到的性能问题,可以利用 redis的incr 命令来生成不重复的递增ID。该策略相较于数据库方案,优点为:

  1. 从远程磁盘IO变为为远程内存IO性能有一定提升,毕竟redis号称10w qps

但为了保证唯一性需要费一番功夫,依次讨论redis的各种持久化策略:

  • 不开启 redis 持久化,则redis宕机后会丢失已生成的ID,再生成会导致ID重复
  • 开启 RDBAOF 中非AOF_FSYNC_ALWAYS模式的持久化,可能丢失最近一段时间的ID,一样会出现ID重复
  • 开启 AOF 中AOF_FSYNC_ALWAYS模式的持久化,能保证即使在宕机的情况下也不会出现ID重复,但性能会下降,相较于数据库方案没有太大的优势

号段模式

号段模式是为了解决数据库自增主键和redis incr方案中,每次获取ID都需要远程请求的问题

即每次从db获取一个ID范围,作为一个号段加载到内存,这样生成唯一ID时不需要每次都从数据库获取,而是从本地内存里获取,大大提高性能。本地缓存的号段用完时才请求db获取下一批号段

以mysql为例,数据库表结构如下:

CREATE TABLE unique_id ( 
    id bigint(20) NOT NULL, 
    max_id bigint(20) NOT NULL COMMENT '下个号段从哪开始分配', 
    step int(10) NOT NULL COMMENT '一批ID的数量', 
    PRIMARY KEY (`id`) 

) ENGINE=InnoDB DEFAULT

每次获取一个号段时,执行如下sql:

update unique_id set max_id = {max_id} + step  where max_id = {max_id} 

当执行成功后判断 affectRows等于1,就能保证只有当前实例获得了 [max_id,max_id + step) 这个区间的号段,就能愉快地在内存发号了

若affectRows不等于1,说明有其他实例获取了这个号段,需要重试再次获取

号段模式的优点如下:

  1. 性能很高:大部分情况下直接在内存发号,无需远程请求,号段范围越大,远程请求的比例越低
  2. 可用性较高:若数据库宕机,可以使用之前获取的号段进行发号,号段范围越大,能撑的时间越久
  3. 趋势递增

缺点如下:

  1. 号段浪费:若某实例在号段没用完时就重启或宕机,则其号段剩余的ID就浪费了。解决方案为较小号段长度,但根据优点中的描述,这会降低性能和可用性,因此需要选择一个适中的号段长度

  2. 不够平滑:当号段用完时,会请求一次数据库,如果此时网络抖动,会使得该次请求响应较慢

    1. 为了解决这种情况,可以在号段即将用完时就异步请求数据库获取下一个号段,而不是等到要用完再请求
  3. 可用性问题:和数据库自增键策略一样的单点问题


如何解决上面提到的可用性问题呢?使用多台实例。这里还是以2台实例为例进行说明:

每次生获取完号段后,将max_id增加 号段长度 * 实例数量

例如初始化时,mysql1.max_id=0,mysql2.max_id=1000。step都是1000

  • 第一次访问mysql1,获取[0:1000)的号段,将mysql1.max_id更新成2000
  • 第二次访问mysql2,获取[1000:2000)的号段,将mysql2.max_id更新成3000
  • 第三次访问mysql1,获取[2000:3000)的号段,将mysql1.max_id更新成4000
  • 第四次访问mysql2,获取[3000:4000)的号段,将mysql2.max_id更新成5000

这样既降低了单台数据库实例的访问压力,又提高了可用性

buffer数量设为多大?峰值qps的600倍,这样db宕机还能提供至少10分钟的服务(容灾性高)


优点:

  1. id位64bit的数字趋势递增
  2. 对db的压力小
  3. 可以很方便线性扩展:按照bizkey分库分表即可
  4. 高可用:内部有缓存,如果数量为峰值qps的600倍,那么db宕机10分钟内都可用
  5. id可以做到几乎单调递增
  6. 可自定义maxId大小,方便原有业务迁移

缺点:

  1. TP999波动大:其他时候查本地缓存,但当号段用完时要查读写一次db
    1. 解决:双buffer,例如当第一个buffer用到10%时就异步请求第二个buffer的号段
  2. id不够随机,泄露发号数量

雪花算法

雪花算法使用一个64位的数字来表示唯一ID,而这64位中的每一位怎么用,就是其精髓所在

标准的雪花算法每一位含义如下:

在这里插入图片描述

  • [0:0] 1位符号位:ID一般为正数,所以该位为0
  • [1:41] 41位时间:通常用来表示 当前时间 - 业务开始时间 的时间差,而不是相对于1970年的时间戳,这样能支持的时间更久,若41位时间戳的单位为毫秒,则能支持大约(1 << 41) / (1000 * 60 * 60 * 24 * 365) = 69年
  • [42:51] 10位机器:一般机器数没那么多,可以将 10位中分5位给机房,分5位给机器。这样就可以表示32个机房,每个机房下可以有32台机器
  • [52:63] 12位自增序列号:表示某台机器上在某一毫秒(如果表示时间的单位为毫秒)内的生成的ID序列号,每毫秒支持1<<12 = 4096个ID,按照 qps算有409.6万

位的分配可以根据业务的不同进行调整,例如若机器数没那么多,不需要10位表示,可增大时间位,以支持更长的时间范围。或者业务并发量不高时,可将时间单位改为秒,将节省出来的位用于表示其他含义

只要每个实例的机器ID不同,则不同机器间生成的ID一定不同,因为其[42:51]位不一样

这样划分后,在一毫秒内一个数据中心的一台机器上可以产生4096个的不重复的ID

其优点为:

  1. 不依赖数据库,性能和可用性非常好
  2. 如果时间戳在高位,能保证ID趋势递增
  3. 理论上支持超高的并发,因为qps有409万,基本不可能有业务的写操作能达到这个qps

缺点为:

  1. ID的生成强依赖于服务器时钟,如果发生时钟回拨,则可能和以前生成过的ID产生冲突

时钟回拨:硬件时钟可能会因为各种原因发生不准的情况,网络中提供了ntp服务来做时间校准,做校准的时候就会发生时钟的跳跃或者回拨的问题

  1. 10位的机器号较难指定,最好不要手工指定,而是实例去自动获取

针对时钟回拨问题,可分两种情况讨论:
  • 实例运行过程中发生时钟回拨:此时可以在内存中 记录上次时间戳,若这次获取的时间戳比上次小,说明发生了时钟回拨,可以等待一段时间再进行ID生成,若回拨幅度较大,则可选择继续等待,或给上层报错,因为在短时间内无法生成正确的ID

也可以完全不依赖系统时间,例如百度的uid-generator使用一个原子变量,每次加一来生成下一个时间

  • 实例重启过程中发生时钟回拨:此时没办法从内存中获取上次的时间戳,因此需要将上次时间戳放到外部存储中。美团leaf的方案为,每3s往zookeeper上报一次当前时间戳,这样在实例重启时,也能判断出是否发生了时钟回拨

但存在外部不能完全避免时钟回拨,例如在t时刻将t保存在zk,在 t+1 时刻分配了一个ID,在 t+2 时刻宕机,时钟回退了1s到t+1,此时检测 t+1 > zk 中的时间t,没问题。但依然会产生 t+1 时刻的重复ID

因此最保险的办法是:宕机后sleep一段时间再重启,这段时间要超过时钟回退的时间


针对机器号生成困难问题:有以下几种解决方案:
  • 使用zookeeper:每次实例启动时,都去zookeeper下创建一个节点,利用其节点编号当做机器id,zookeeper保证每次生成的节点编号唯一

  • 使用mysql:也可以在实例启动时,去数据库的表插入一条记录,利用自增主键当做机器ID,同样能保证机器ID的唯一性

适用场景:订单中,不想让别人根据早上和晚上的订单id号猜到销量的场景

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

大语言模型LLMs在医学领域的最新进展总结

我是娜姐 迪娜学姐 &#xff0c;一个SCI医学期刊编辑&#xff0c;探索用AI工具提效论文写作和发表。 相比其他学科&#xff0c;医学AI&#xff0c;是发表学术成果最多的领域。 医学数据的多样性和复杂性&#xff08;包括文本、图像、基因组数据等&#xff09;&#xff0c;使得…

Vue 学习随笔系列十四 -- JavaScript巧妙用法

JavaScript巧妙用法 文章目录 JavaScript巧妙用法1、String.padStart 函数2、String.padEnd 函数3、tirm 函数3. Object.freeze 函数4. Object.fromEntries 函数5. Object.entries 函数6. Array.prototype.flat 函数 1、String.padStart 函数 在字符串前面进行填充 let temp …

【PGCCC】Postgresql 物理流复制

postgresql 提供了主从复制功能&#xff0c;有基于文件的拷贝和基于 tcp 流的数据传输两种方式。两种方式都是传输 wal 数据&#xff0c;前者是等待生成一个完整的wal文件后&#xff0c;才会触发传输&#xff0c;后者是实时传输的。可以看出来基于文件方式的延迟会比较高&#…

每日小练:Day2

1.乒乓球筐 题目链接&#xff1a;乒乓球筐__牛客网 题目描述&#xff1a; 这道题主要考察B盒是不是A盒的子集&#xff0c;我们可以通过哈希表来做 单哈希表 import java.util.Scanner;// 注意类名必须为 Main, 不要有任何 package xxx 信息 public class Main {public stat…

esp32学习:如何解决OV5640摄像头发热问题

我们在使用esp开发板过程中&#xff0c;连接ov2640摄像头时&#xff0c;非常正常&#xff0c;但连接ov5640摄像头时&#xff0c;会发现摄像头发烫&#xff0c;非常热&#xff0c;我们网上找解决方案&#xff0c;基本都是加散热片&#xff0c;没有根本解决问题。 前段时间&#…

JQuery封装的ajax

1. 注意&#xff1a; 首先要导jq的包json对象可以用 . 来调用keyjava只能给前端传页面&#xff0c;或者打印的内容String jsonstr json.toJSONString(resultJSON); //将对象转为JSON对象 Json格式和参数解释&#xff1a; <script src"js/jquery-1.10.2.min.js&quo…

【计算机网络】章节 知识点总结

一、计算机网络概述 1. 计算机网络向用户提供的两个最重要的功能&#xff1a;连通性、共享 2. 因特网发展的三个阶段&#xff1a; 第一阶段&#xff1a;从单个网络 ARPANET 向互联网发展的过程。1983 年 TCP/IP 协议成为 ARPANET 上的标准协议。第二阶段&#xff1a;建成三级…

Python+robotframework接口自动化测试实操(超详细总结)

&#x1f345; 点击文末小卡片 &#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 目前我们需要考虑的是如何实现关键字驱动实现接口自动化输出&#xff0c;通过关键字的封装实现一定意义上的脚本与用例的脱离&#xff01; robot framework 的…

如何管理好自己的LabVIEW项目

在LabVIEW项目开发中&#xff0c;项目管理对于提高开发效率、确保项目质量、减少错误和维护成本至关重要。以下从项目规划、代码管理、测试与调试、版本控制、团队协作等方面&#xff0c;分享LabVIEW项目管理的体会。 ​ 1. 项目规划与需求分析 关键步骤&#xff1a; 需求分析…

【快速解决】kafka崩了,重启之后,想继续消费,怎么做?

目录 一、怎么寻找我们关心的主题在崩溃之前消费到了哪里&#xff1f; 1、一个问题&#xff1a; 2、查看消费者消费主题__consumer_offsets 3、一个重要前提&#xff1a;消费时要提交offset 二、指定 Offset 消费 假如遇到kafka崩了&#xff0c;你重启kafka之后&#xff0…

matlab建模入门指导

本文以水池中鸡蛋温度随时间的变化为切入点&#xff0c;对其进行数学建模并进行MATLAB求解&#xff0c;以更为通俗地进行数学建模问题入门指导。 一、问题简述 一个煮熟的鸡蛋有98摄氏度&#xff0c;将它放在18摄氏度的水池中&#xff0c;五分钟后鸡蛋的温度为38摄氏度&#x…

51单片机应用开发(进阶)---定时器应用(电子时钟)

实现目标 1、巩固定时器的配置流程&#xff1b; 2、掌握按键、数码管与定时器配合使用&#xff1b; 3、功能1&#xff1a;&#xff08;1&#xff09;简单显示时间。显示格式&#xff1a;88-88-88&#xff08;时-分-秒&#xff09; 4、功能2&#xff1a;&#xff08;1&#…

【外包】软件行业的原始形态,项目外包与独立开发者

【外包】互联网软件行业的原始形态&#xff0c;项目外包与独立开发者 本科期间写的一些东西&#xff0c;最近整理东西看到了&#xff0c;大致整理一下放出来&#xff0c;部分内容来自其他文章&#xff0c;均已引用。 文章目录 1、互联网软件行业的原始形态2、项目订单&#xff…

Python酷库之旅-第三方库Pandas(208)

目录 一、用法精讲 971、pandas.MultiIndex.set_levels方法 971-1、语法 971-2、参数 971-3、功能 971-4、返回值 971-5、说明 971-6、用法 971-6-1、数据准备 971-6-2、代码示例 971-6-3、结果输出 972、pandas.MultiIndex.from_arrays类方法 972-1、语法 972-2…

基于ConvNeXt的矿石种类识别

项目源码获取方式见文章末尾&#xff01; 600多个深度学习项目资料&#xff0c;快来加入社群一起学习吧。 《------往期经典推荐------》 项目名称 1.【基于CNN-RNN的影像报告生成】 2.【卫星图像道路检测DeepLabV3Plus模型】 3.【GAN模型实现二次元头像生成】 4.【CNN模型实现…

C++入门基础知识140—【关于C++ 类构造函数 析构函数】

成长路上不孤单&#x1f60a;&#x1f60a;&#x1f60a;&#x1f60a;&#x1f60a;&#x1f60a; 【14后&#x1f60a;///C爱好者&#x1f60a;///持续分享所学&#x1f60a;///如有需要欢迎收藏转发///&#x1f60a;】 今日分享关于C 类构造函数 & 析构函数的相关内容…

【Linux】-学习笔记03

第十一章-管理Linux软件包和进程 1.源码下载安装软件 1.1概念 源码文件&#xff1a;程序编写者使用C或C等语言编写的原始代码文本文件 源码文件使用.tar.gz或.tar.bz2打包成压缩文件 1.2特点 源码包可移植性好&#xff0c;与待安装软件的工作环境依赖性不大 由于有编译过程…

鸿蒙HarmonyOS(ArkUI基础篇大合集!)

文章目录 ArkUI&#xff08;方舟UI框架&#xff09;1.简介2.基本概念3.概述4.布局1.概述2.通用布局属性&#x1f388;1.盒子属性2.背景属性3.定位属性4.通用属性&#x1f388; 3.线性布局4.弹性布局(Flex)5.层叠布局(Stack) 5.组件1.使用文本1.文本显示(Text/Span)2.文本输入 (…

Prompt 工程

Prompt 工程 1. Prompt 工程简介 “预训练-提示预测”范式是近年来自然语言处理&#xff08;NLP&#xff09;领域的一个重要趋势&#xff0c;它与传统的“预训练-微调-预测”范式相比&#xff0c;提供了一种更为灵活和高效的模型应用方式。 Prompt工程是指在预训练的大型语言…

十天入门javaScript第四天(Promises对象异步 )(睡眠函数) (json)

Promise 是一个 JavaScript 的内置对象&#xff0c;它代表了一个异步操作的最终完成&#xff08;或失败&#xff09;及其结果值。Promise 对象是异步编程的一种解决方案&#xff0c;它可以使异步操作以更简洁、更易于管理的方式进行。 Promise 对象有三个状态&#xff1a; Pen…