分布式唯一ID生成方案

目录

一、前言

二、分布式ID的几种生成方案

2.1、UUID

2.2、MySQL主键自增

2.3、MySQL多实例主键自增

2.4、雪花snowflake算法

2.5、Redis生成方案

2.6、小结

三、一线大厂是如何设计的呢?

3.1、改造数据库主键自增

3.2、竞争问题

3.3、突发阻塞问题

3.4、双buffer方案

四、总结


一、前言

分布式系统中我们会对一些数据量大的业务进行分拆,如:用户表,订单表。因为数据量巨大一张表无法承接,就会对其进行分库分表。小伙伴们可以去看一下《数据库分库分表永不迁移数据和避免热点

但一旦涉及到分库分表,就会引申出分布式系统中唯一主键ID的生成问题,永不迁移数据和避免热点的文章中要求需要唯一ID的特性:

  • 整个系统ID唯一

  • ID是数字类型,而且是趋势递增的

  • ID简短,查询效率快

什么是递增?如:第一次生成的ID为12,下一次生成的ID是13,再下一次生成的ID是14。这个就是生成ID递增。

什么是趋势递增?如:在一段时间内,生成的ID是递增的趋势。如:在一段时间内生成的ID在【0,1000】之间,过段时间生成的ID在【1000,2000】之间。但在【0-1000】区间内的时候,ID生成有可能第一次是12,第二次是10,第三次是14。

那有什么方案呢?往下看!

二、分布式ID的几种生成方案

2.1、UUID

这个方案是小伙伴们第一个能够考虑到的方案

优点:

  • 代码实现简单。

  • 本机生成,没有性能问题

  • 因为是全球唯一的ID,所以迁移数据容易

缺点:

  • 每次生成的ID是无序的,无法保证趋势递增

  • UUID是字符串存储,查询效率慢

  • 存储空间大

  • ID本身无业务含义,不可读

应用场景:

  • 类似生成token令牌的场景

  • 不适用一些要求有趋势递增的ID场景

此UUID方案是不适用老顾的需求。

2.2、MySQL主键自增

这个方案就是利用了MySQL的主键自增auto_increment,默认每次ID加1。

优点:

  • 数字化,id递增

  • 查询效率高

  • 具有一定的业务可读

缺点:

  • 存在单点问题,如果mysql挂了,就没法生成iD了

  • 数据库压力大,高并发抗不住

2.3、MySQL多实例主键自增

这个方案就是解决mysql的单点问题,在auto_increment基本上面,设置step步长

每台的初始值分别为1,2,3...N,步长为N(这个案例步长为4)

优点:

  • 解决了单点问题

缺点:

  • 一旦把步长定好后,就无法扩容;而且单个数据库的压力大,数据库自身性能无法满足高并发

应用场景:

  • 数据不需要扩容的场景

此方案也不满足老顾的需求,因为不方便扩容(记住这个方案,嘿嘿)

2.4、雪花snowflake算法

这个算法网上介绍了很多,老顾这里就不详细介绍。雪花算法生成64位的二进制正整数,然后转换成10进制的数。64位二进制数由如下部分组成:

  • 1位标识符:始终是0

  • 41位时间戳:41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截 )得到的值,这里的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的

  • 10位机器标识码:可以部署在1024个节点,如果机器分机房(IDC)部署,这10位可以由 5位机房ID + 5位机器ID 组成

  • 12位序列:毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号

优点:

  • 此方案每秒能够产生409.6万个ID,性能快

  • 时间戳在高位,自增序列在低位,整个ID是趋势递增的,按照时间有序递增

  • 灵活度高,可以根据业务需求,调整bit位的划分,满足不同的需求

缺点:

  • 依赖机器的时钟,如果服务器时钟回拨,会导致重复ID生成

在分布式场景中,服务器时钟回拨会经常遇到,一般存在10ms之间的回拨;小伙伴们就说这点10ms,很短可以不考虑吧。但此算法就是建立在毫秒级别的生成方案,一旦回拨,就很有可能存在重复ID。

此方案暂不符合老顾的需求(嘿嘿,看看怎么优化这个方案,小伙伴们先记住)

2.5、Redis生成方案

利用redis的incr原子性操作自增,一般算法为:

年份 + 当天距当年第多少天 + 天数 + 小时 + redis自增

优点:

  • 有序递增,可读性强

缺点:

  • 占用带宽,每次要向redis进行请求

整体测试了这个性能如下:

需求:同时10万个请求获取ID

1、并发执行完耗时:9s左右
2、单任务平均耗时:74ms
3、单线程最小耗时:不到1ms
4、单线程最大耗时:4.1s

性能还可以,如果对性能要求不是太高的话,这个方案基本符合老顾的要求。

但不完全符合业务老顾希望id从 1 开始趋势递增。(当然算法可以调整为 就一个 redis自增,不需要什么年份,多少天等)。

2.6、小结

以上介绍了常见的几种分布式ID生成方案。一线大厂的分布式ID方案绝没有这个简单,他们对高并发,高可用的要求很高。

如Redis方案中,每次都要去Redis去请求,有网络请求耗时,并发强依赖了Redis。这个设计是有风险的,一旦Redis挂了,整个系统不可用。

而且一线大厂也会考虑到ID安全性的问题,如:Redis方案中,用户是可以预测下一个ID号是多少,因为算法是递增的。

这样的话竞争对手第一天中午12点下个订单,就可以看到平台的订单ID是多少,第二天中午12点再下一单,又看到平台订单ID到多少。这样就可以猜到平台1天能产生多少订单了,这个是绝对不允许的,公司绝密啊。

三、一线大厂是如何设计的呢?

一线大厂的设计思路其实和小伙伴们思路差不多,只是多想了1~2层,设计上面多了1~2个环节。

3.1、改造数据库主键自增

上述我们介绍了利用数据库的自增主键的特性,可以实现分布式ID;这个ID比较简短明了,适合做userId,正好符合如何永不迁移数据和避免热点? 根据服务器指标分配数据量(揭秘篇)文章中的ID的需求。但这个方案有严重的问题:

  • 一旦步长定下来,不容易扩容

  • 数据库压力山大

小伙伴们看看怎么优化这个方案。先看数据库压力大,为什么压力大?是因为我们每次获取ID的时候,都要去数据库请求一次。那我们可不可以不要每次去取?

思路是我们请求数据库得到ID的时候,可设计成获得的ID是一个ID区间段。

我们看上图,有张ID规则表:

1、id表示为主键,无业务含义。

2、biz_tag为了表示业务,因为整体系统中会有很多业务需要生成ID,这样可以共用一张表维护

3、max_id表示现在整体系统中已经分配的最大ID

4、desc描述

5、update_time表示每次取的ID时间

我们再来看看整体流程:

1、【用户服务】在注册一个用户时,需要一个用户ID;会请求【生成ID服务(是独立的应用)】的接口

2、【生成ID服务】会去查询数据库,找到user_tag的id,现在的max_id为0,step=1000

3、【生成ID服务】把max_id和step返回给【用户服务】;并且把max_id更新为max_id = max_id + step,即更新为1000

4、【用户服务】获得max_id=0,step=1000;

5、 这个用户服务可以用ID=【max_id + 1,max_id+step】区间的ID,即为【1,1000】

6、【用户服务】会把这个区间保存到jvm中

7、【用户服务】需要用到ID的时候,在区间【1,1000】中依次获取id,可采用AtomicLong中的getAndIncrement方法。

8、如果把区间的值用完了,再去请求【生产ID服务】接口,获取到max_id为1000,即可以用【max_id + 1,max_id+step】区间的ID,即为【1001,2000】

这个方案就非常完美的解决了数据库自增的问题,而且可以自行定义max_id的起点,和step步长,非常方便扩容。

而且也解决了数据库压力的问题,因为在一段区间内,是在jvm内存中获取的,而不需要每次请求数据库。即使数据库宕机了,系统也不受影响,ID还能维持一段时间。

3.2、竞争问题

以上方案中,如果是多个用户服务,同时获取ID,同时去请求【ID服务】,在获取max_id的时候会存在并发问题。

如用户服务A,取到的max_id=1000 ;用户服务B取到的也是max_id=1000,那就出现了问题,Id重复了。那怎么解决?

其实方案很多,加分布式锁,保证同一时刻只有一个用户服务获取max_id。当然也可以用数据库自身的锁去解决。

利用事务方式加行锁,上面的语句,在没有执行完之前,是不允许第二个用户服务请求过来的,第二个请求只能阻塞。

3.3、突发阻塞问题

上图中,多个用户服务获取到了各自的ID区间,在高并发场景下,ID用的很快,如果3个用户服务在某一时刻都用完了,同时去请求【ID服务】。因为上面提到的竞争问题,所有只有一个用户服务去操作数据库,其他二个会被阻塞。

小伙伴就会问,有这么巧吗?同时ID用完。我们这里举的是3个用户服务,感觉概率不大;如果是100个用户服务呢?概率是不是一下子大了。

出现的现象就是一会儿突然系统耗时变长,一会儿好了,就是这个原因导致的,怎么去解决?

3.4、双buffer方案

在一般的系统设计中,双buffer会经常看到,怎么去解决上面的问题也可以采用双buffer方案。

在设计的时候,采用双buffer方案,上图的流程:

1、当前获取ID在buffer1中,每次获取ID在buffer1中获取

2、当buffer1中的Id已经使用到了100,也就是达到区间的10%

3、达到了10%,先判断buffer2中有没有去获取过,如果没有就立即发起请求获取ID线程,此线程把获取到的ID,设置到buffer2中。

4、如果buffer1用完了,会自动切换到buffer2

5、buffer2用到10%了,也会启动线程再次获取,设置到buffer1中

6、依次往返

双buffer的方案,小伙伴们有没有感觉很酷,这样就达到了业务场景用的ID,都是在jvm内存中获得的,从此不需要到数据库中获取了。允许数据库宕机时间更长了。

因为会有一个线程,会观察什么时候去自动获取。两个buffer之间自行切换使用。就解决了突发阻塞的问题。

四、总结

此方案是某团使用的分布式ID算法,小伙伴们如果想了解更深,可以去网上搜下,这里应该介绍了比较详细了。

当然此方案美团还做了一些别的优化,监控ID使用频率,自动设置步长step,从而达到对ID节省使用。

此ID方案非常适合《数据库分库分表永不迁移数据和避免热点》中的ID需求。

但此ID存在一定的问题,就是太过连续,竞争对手可以预测,不适合订单ID。我们在下一篇文章中继续介绍,敬请期待!

<think> 首先,用户的问题是关于Snowflake算法的介绍。用户要求了解Snowflake算法的相关信息,包括它是Twitter开源的分布式唯一ID生成方案,能生成全局唯一且趋势递增的64位长整型ID。 根据系统指令: - 所有行内数学表达式必须使用$...$格式。 - 独立公式必须使用$$...$$格式并单独成段。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题。 - 回答中引用的段落末尾自然地添加引用标识。 - 回答结构清晰,帮助用户逐步解决问题,保证回答真实可靠。 用户提供的引用包括: - 引用[1]: 描述了Snowflake算法的构成:64位ID由符号位、时间戳、机器ID和序列号组成。 - 引用[2]: 解释了为什么需要雪花算法,对比传统方案的局限性,以及其核心优势:全局唯一、趋势递增、高性能、轻量级。 - 引用[3]: 涉及性能优化、与UUID的对比,以及典型应用场景。 我需要基于这些引用构建回答,同时确保内容准确。Snowflake算法的标准结构是: - 64位ID划分为: - 1位符号位(通常为0)。 - 41位时间戳(毫秒级)。 - 10位机器ID(可以细分为数据中心ID和机器ID)。 - 12位序列号。 在回答中,我应该逐步介绍算法: 1. **简介**:说明Snowflake算法是什么,是谁开发的,以及其目的。 2. **核心结构**:详细解释64位ID的各个组成部分。 3. **工作原理**:描述如何生成ID。 4. **优势**:对比传统方法,突出其优点。 5. **潜在问题**:例如时钟回拨问题。 6. **应用场景**:举例说明使用场景。 7. **生成相关问题**:在末尾添加相关问题。 确保使用中文,结构清晰。对于数学内容,使用LaTeX格式: - 例如,时间戳部分可以用$ \text{时间戳} $。 - 独立公式如总位数的计算可以用$$...$$。 最后,添加引用标识[^1], [^2], [^3]在相关段落末尾。 现在,规划回答结构: 1. **引言**:介绍Snowflake算法。 2. **核心组成**:详细描述64位ID的结构。 3. **生成过程**:解释如何生成ID。 4. **优势与局限性**:讨论优点和潜在问题。 5. **应用场景**:列举常见应用。 6. **相关问题**:生成3-5个相关问题。 在引用部分末尾添加标识。 </think>### Snowflake算法介绍 Snowflake算法(雪花算法)是由Twitter开发的一种分布式唯一ID生成方案,专为高并发分布式系统设计。它能生成一个全局唯一且趋势递增的64位长整型ID(例如:1234567890123456),适用于订单号、用户ID、日志追踪等场景。该算法的核心优势在于本地生成、高性能(每秒可生成数百万ID)、轻量级(无需依赖数据库或外部服务),并且ID的趋势递增特性有利于数据库索引优化。以下我将从核心结构、生成过程、优势和局限性等方面逐步解析Snowflake算法,确保内容基于可靠技术文档[^1][^2]。 #### 1. **Snowflake算法的核心结构** Snowflake生成的64位ID由四个固定部分组成,每个部分占用特定比特位,确保ID唯一性和有序性。结构如下(使用位运算表示): - **符号位(1位)**:固定为0,保留用于未来扩展(例如支持负数ID)。 - **时间戳(41位)**:记录ID生成时的毫秒级时间戳,相对于一个自定义纪元(如Twitter使用2010年1月1日)。时间戳的计算公式为: $$ \text{时间戳} = \text{当前时间} - \text{纪元时间} $$ 例如,如果当前时间戳为$t$毫秒,纪元时间为$e$毫秒,则时间戳部分为$t - e$[^1]。 - **机器ID(10位)**:标识生成ID的机器或节点,通常划分为数据中心ID(5位)和机器ID(5位),支持最多$2^{10} = 1024$个节点。例如,数据中心ID$d$和机器ID$m$组合为$d \times 32 + m$[^2]。 - **序列号(12位)**:在同一个毫秒内的递增序列,支持每个节点每毫秒生成最多$2^{12} = 4096$个ID。序列号从0开始,当同一毫秒内ID生成超过上限时,会等待到下一毫秒[^1][^3]。 整体ID结构可以用二进制表示: $$ \text{ID} = (\text{符号位} \ll 63) \, | \, (\text{时间戳} \ll 22) \, | \, (\text{机器ID} \ll 12) \, | \, \text{序列号} $$ 其中$ \ll $表示左移运算。例如,一个ID生成过程涉及位运算的组合[^1]。 #### 2. **ID生成过程** Snowflake算法在本地生成ID,无需网络请求,过程高效且线程安全: 1. **获取当前时间戳**:以毫秒为单位计算$ \text{当前时间} - \text{纪元时间} $。 2. **检查时钟回拨**:如果当前时间小于上次生成ID的时间(时钟回拨问题),会抛出异常或等待时钟同步(这是算法的潜在缺陷)。 3. **处理序列号**:如果当前毫秒内序列号达到上限(4096),则休眠至下一毫秒并重置序列号为0。 4. **组合ID**:通过位运算将符号位、时间戳、机器ID和序列号拼接为64位整数。 以下是一个简化的Python伪代码示例(基于标准实现): ```python import time class SnowflakeGenerator: def __init__(self, machine_id, epoch=1288834974657): # Twitter纪元时间(2010-01-01) self.machine_id = machine_id # 机器ID(0-1023) self.epoch = epoch self.sequence = 0 self.last_timestamp = -1 def generate_id(self): current_timestamp = int(time.time() * 1000) - self.epoch if current_timestamp < self.last_timestamp: raise Exception("时钟回拨错误") if current_timestamp == self.last_timestamp: self.sequence = (self.sequence + 1) & 0xFFF # 序列号范围0-4095 if self.sequence == 0: # 当前毫秒序列号耗尽 current_timestamp = self.wait_next_millis(self.last_timestamp) else: self.sequence = 0 self.last_timestamp = current_timestamp return (current_timestamp << 22) | (self.machine_id << 12) | self.sequence ``` #### 3. **优势和局限性** **优势**: - **全局唯一**:通过机器ID和时间戳保证ID分布式系统中不冲突。 - **趋势递增**:时间戳在前,ID自然有序,优化数据库索引(如B+树索引),比UUID的随机性更高效[^2][^3]。 - **高性能**:本地生成避免网络延迟,每秒可处理百万级请求(如Twitter实测QPS > 400k)[^3]。 - **轻量级**:仅需配置机器ID,无需额外依赖如数据库或Redis[^2]。 **局限性**: - **时钟回拨问题**:如果系统时间倒退(如NTP同步),可能导致ID重复(需通过异常处理或硬件时钟解决)。 - **机器ID分配**:需要手动管理机器ID,避免冲突(通常通过配置中心或ZooKeeper实现)。 - **位宽限制**:时间戳41位支持约69年($2^{41} / (1000 \times 60 \times 60 \times 24 \times 365) \approx 69$年),序列号12位限制单节点每毫秒4096个ID,在极端高并发下可能成瓶颈[^1][^3]。 与传统方案对比(基于引用[3]): | 特性 | Snowflake算法 | UUID | |---------------|------------------------|------------------------| | ID长度 | 64位(8字节) | 128位(16字节) | | 趋势性 | 趋势递增 | 随机无序 | | 存储空间 | 小,适合密集存储 | 大,影响索引效率 | | 生成方式 | 本地生成 | 本地生成 | | 时钟依赖 | 依赖系统时间 | 不依赖 | #### 4. **典型应用场景** Snowflake算法广泛应用于分布式系统: - **电商平台**:生成订单号、支付ID,确保唯一性和查询效率。 - **社交网络**:用户ID和消息ID,支持高并发用户增长。 - **日志系统**:追踪请求链路的唯一标识(如TraceID)。 - **分布式锁**:作为锁的唯一键,避免死锁[^2][^3]。 通过合理配置机器ID和监控时钟,Snowflake算法能高效支撑大规模分布式应用[^1][^2]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值