探索生成分布式ID的5种独特方法,让你的项目脱颖而出!

介绍

#分布式系统#一般来说,像一些单机或者单数据库的项目规模比较小的系统,该平台流量和业务量较小,业务ID生成方式虽然比较原始但足够了。但对于大规模复杂业务、分布式、高并发的应用场景,这种ID生成方式显然无法像小项目那样单纯依靠简单的数据自增序列来完成。

而且,在分布式环境下,这种方式已经不能满足业务的需求,不仅不能保证业务需求,而且业务ID生成速度过快或者重复可能会导致系统严重故障。

所以就有了分布式唯一ID的生成方法。

分布式 ID的特点

在分析之前,我们先明确一下业务ID的生成特征。基于这些特点,我们可以对后面的生成方法有更深入的认识和认识。

  • 全局唯一:这是基本要求,不可重复。
  • 数字类型,增加趋势:后一个ID必须大于前一个ID。这是从MySQL存储引擎考虑的,需要保证写入数据的性能。
  • 长度短:可以提高查询效率,这也是基于MySQL数据库规范,特别是当ID作为主键时。
  • 信息安全:如果ID不断生成,业务信息必然会被泄露,甚至可能被猜到,所以需要不规则。
  • 高可用、低延迟:ID生成速度快,可以承受高并发,并且延迟足够低,不会成为业务瓶颈。

生成分布式ID的几种方法

1.基于UUID

这是一个简单的解决方案。毕竟UUID在全球独一无二的特性已经深入人心。但是,任何熟悉 MySQL 数据库功能的人都不应该使用此作为业务 ID。

它不可读而且太长,在这里不是一个好主意,除非您的系统足够小而不关心这一点.

下面我们简单总结一下使用UUID作为业务ID的优缺点,以及该方式适用的业务场景。

优势:

  • 代码实现很简单
  • 本地构建不存在性能问题。
  • 由于具有全局唯一性,因此数据库迁移没有问题。

缺点

  • 每次生成的ID都是无序的,并非都是数字,不能保证趋势会增加。
  • UUID生成字符串,字符串存储性能较差,查询效率慢。
  • UUID的长度太长,不适合存储,并且消耗数据库性能。
  • ID没有一定的商业意义,可读性较差。

适用场景:

  • 它可以用来生成token等场景,这些场景具有足够的不可识别性、无序性和可读性,并且有足够的长度。
  • 可以用在对纯数字、自增、可读性要求不高的场景。

2.根据数据库主键自动递增

比较常用的是利用数据库主键自增的方法。

以MySQL为例,在创建新表时,指定主键将通过auto_increment自动生成,或者指定一个增长步长。

这对于小型单机部署的业务系统来说已经足够了,使用简单,具有一定的业务性质,但在分布式高并发系统中就不太适用了。

分布式系统涉及分库、分表。在跨机器甚至跨机房部署的环境下,数据库的自增方式无法满足业务需求。同时,在高并发访问、大量访问的情况下,数据库的容量是有限的。

优势:

  • 实现简单,仅依赖数据库,成本较低。
  • ID被数字化并单调递增,以满足数据库存储和查询性能。
  • 具有一定的业务可读性。

缺点

  • 对DB依赖强,存在单点问题,如果数据库宕机,业务不可用。
  • DB生成ID性能有限,单点数据库压力大,无法处理高并发场景。

适用场景:

  • 业务规模小、数据访问量少的场景。
  • 不存在高并发场景,插入记录可控。

3.基于数据库多实例主键自增。

上面我们粗略的解释了数据库的主键是怎样的auto-incremented,并讨论了单机部署的情况。

如果想提高ID生成的效率,可以通过水平扩展机器来平衡单点数据库的压力。

这个计划是如何实施的?

即在auto_increment的基础上,设置step增长步长,使得之前DB生成的ID趋势增大且不重复。

从上图可以看出,水平扩展的数据库集群有利于解决数据库的单点压力问题。同时,对于ID生成,根据机器数量设置自动递增步长。

但是,这里有一个缺点,就是无法再扩展。如果再次扩容,则无法生成ID,步长也会被用完。

那么如果想解决新机器带来的问题,可以将第三台机器的ID生成位置设置到距离当前ID较远的位置,在其中设置新的步长,并在第三台机器上修改旧的步长。同时。机器上生成ID的步长,但在ID增长到新机器设置的值之前必须先递增ID,否则会重复。

优势:

  • 解决了负载均衡时ID生成的单点问题。

缺点

  • 一定要确定好步长,这会给后续的扩展带来困难,而且单个数据库本身的压力还是很大,无法满足高并发。

适用场景:

  • 数据量不大,数据库不需要扩展。

该方案除了难以适应大规模分布式、高并发场景外,普通业务规模还是能够胜任的,因此该方案值得积累。

4.基于Snowflake算法

Snowflake算法是twitter内部分布式项目使用的ID生成算法。它现在是开源的并且很受欢迎。下图是Snowflake算法的ID组成图。

该方案巧妙地将64位分成多段,分别表示时间戳、机器标识和随机序列,首先生成64位二进制正整数,然后将其转换为十进制存储。

  • 其中,1-bit是标记位,未使用,标记为0。
  • 时间戳41-bit用于存储时间戳。
  • 机器码10-bit可以识别1024个机器节点。如果机器部署在IDC,这10位也可以拆分。比如5位代表机房ID,5位代表机器ID,这样就有32*32组合。,一般来说就足够了。
  • 最后一个12-bit随机序列用于记录以毫秒为单位的计数,一个节点可以生成4096个ID序列号。

所以综上所述,经过综合计算,Snowflake算法方案的理论QPS约为410万/s,性能足够强,并且该方法可以保证集群中每个节点生成的ID不同。

优势:

  • 它每秒可以生成数百万个不同的ID,并且性能良好。
  • 时间戳值在高位,中间是固定的机器码,自增序列在低位,整个ID呈趋势递增。
  • 可以根据业务场景数据库节点的排列灵活划分,灵活性高。

缺点

  • 它强烈依赖于机器时钟。如果时钟拨回,就会产生重复的ID。因此,如果基于此的算法回拨时钟,就会抛出异常,可能会导致服务不可用。

适用场景:

  • 雪花算法的明显缺点是时钟依赖性。如果保证机器没有时钟倒退,则采用该方法生成分布式ID是可行的。

5.基于Redis

Redis的命令INCR可以将key中存储的数字值加一。由于atomic这个操作的性质,我们可以巧妙地利用它来创建分布式ID生成方案,并且还可以与时间戳和机器ID等其他值配合组合使用。

优势:

  • 增量有序,可读性强。
  • 可以满足一定的表演。

缺点

  • 强依赖Redis,可能存在单点问题。
  • 占用网络,需要考虑网络延迟等问题对性能的影响。

适用场景:

  • 性能要求不太高,小规模业务较轻,对Redis的运行有一定要求。注意网络问题和单点压力问题。如果是分布式的情况,需要考虑的问题就更多了。因此,这种方法很少在一组情况下使用。

事实上,Redis的解决方案的可靠性还需要研究。毕竟,这取决于网络。延迟故障或停机可能会导致服务不可用。在系统设计时需要考虑到这种风险。

6.基于LEAF方案。

从以上几种分布式ID方案可以看出,它可以解决一定的问题,但也存在明显的缺陷。为此,美团在数据库方案的基础上进行了优化,提出了名为Leaf-segment的数据库方案。

原来的方案中,我们每次获取ID都需要读取一次数据库,在高并发、大数据量的情况下很容易对数据库造成压力。是否可以一次获取一批ID,从而不需要频繁访问?

Leaf的解决办法是每次获取一个ID段。ID段用完后去数据库获取新的号段,可以大大减轻数据库的压力。怎么做?

很简单,我们创建一个表如下:

sql 复制代码
| Field       | Type         | Null | Key | Default           | Extra                       |
+-------------+--------------+------+-----+-------------------+-----------------------------+
| biz_tag     | varchar(128) | NO   | PRI |                   |                             |
| max_id      | bigint(20)   | NO   |     | 1                 |                             |
| step        | int(11)      | NO   |     | NULL              |                             |
| desc        | varchar(256) | YES  |     | NULL              |                             |
| update_time | timestamp    | NO   |     | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP |
+-------------+--------------+------+-----+-------------------+-----------------------------+

其中,biz_tag用于区分业务,max_id表示当前分配的ID号段的最大值biz_tag,step表示每次分配的号段长度,后面的desc和update_time表示该业务的业务描述和最后更新时间分别为数段。

以前每次拿到ID都需要访问数据库。现在你只需要把 设Step得足够合理,比如1000。现在ID用完后就可以访问数据库了。

现在我们可以这样设计获取分布式ID的整个流程:

  1. 当注册用户时,用户服务需要用户ID,它会请求ID服务的接口(这是一个独立的应用程序)。
  2. 生成 ID 的服务将查询数据库以查找 的 id user_tag,当前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,即1, 1000
  6. 用户服务将此间隔保存到 JVM。
  7. 当用户业务需要使用ID时,可以在区间 内依次获取ID 1, 1000,并使用getAndIncrement中的方法。AtomicLong
  8. 如果该时间间隔的值 ****用完,则到请求生产ID的服务接口,获取 as max_id,1000即可以使用max_id + 1, max_id+step该时间间隔的ID,即1001, 2000

显然,这种方法很好的解决了数据库自增的问题,并且可以自定义max_id的起始点和步长,非常灵活,易于扩展。

同时,这种方法也很好的解决了数据库压力的问题,而且ID号段存储在JVM中,性能得到了很大的保证,可用性也可以接受。

优势:

  • 灵活的扩展和强大的性能可以支持大多数业务场景。
  • ID数量呈增长趋势,满足数据库存储和查询性能要求。
  • 高可用性,即使ID生成服务器不可用,也能在短时间内恢复服务,为故障排除赢得时间。
  • max_id的大小可以自定义,方便业务迁移和机器水平扩展。

缺点

  • ID号不够随机,完全顺序递增可能会带来安全问题。
  • DB宕机可能会导致整个系统不可用,但由于号码段只能维持一段时间,所以仍然存在这种风险。
  • 分布式环境中可能存在各个节点同时竞争ID号段的情况,这可能会导致并发问题和重复生成ID。

上述缺点也需要引起足够的重视。官方技术团队还想出了一个妙招------ double buffer。

双缓冲

如上所述,由于可能有多个节点同时请求ID范围,因此最好避免这种情况。

Leaf-segment对此进行了优化,将获取一个号码段的方式优化为获取两个号码段。一个号码段用完后,无需立即更新号码段。还有缓存号段进行备份,可以有效解决问题。

当当前号段消耗10%时,检查下一个号段是否准备好,如果没有,则更新下一个号段。当当前号段用完时,切换到下一个缓存的号段使用,当下一个号段消耗到10%时,检查下一个号段是否就绪,以此类推。

下面简单总结一下这个过程:

  1. 当前采集ID在buffer1中,每次采集ID都是在buffer1中采集的。
  2. 当 buffer1 中的 Id 已使用到 100(即范围的 10%)时。
  3. 当达到10%时,首先判断是否已经获取到buffer2。如果没有,立即发起请求获取线程ID。该线程将获得的ID设置为buffer2。
  4. 如果buffer1用完,会自动切换到buffer2。
  5. 当buffer2使用到10%时,也会启动线程再次获取并设置到buffer1中来回。

双缓冲方案是经过深思熟虑的。有一个单独的线程来观察下一个缓冲区何时更新。两个缓冲区之间的切换也解决了临时去数据库更新号段可能带来的并发问题。

这种方式可以增加业务ID在JVM中的可用性,建议该段的长度是业务高峰期QPS的100倍(经验值,可以根据自己的业务来设置),这样即使数据库宕机,业务ID也会生成。还可以长期维护,可以有效兼容偶尔出现的网络抖动等问题。

优势:

  • 基本的数据库问题已经解决并且可以运行了。
  • 双缓冲区的数段是基于JVM存储的,减少了数据库查询,减少了网络依赖,效率更高。

缺点

  • 号段的长度是固定的,业务量大时,可能会频繁更新号段,因为原来分配的号段会一下子用完。
  • 如果号码段长度设置过长,如果缓存中的号码段尚未用完,其他节点重新获取的号码段可能会比之前的跨度更大。

针对上述缺陷,官方技术团队重新提出了动态调整号码段长度的方案。

动态调整step

一般情况下,如果你的业务不会出现明显的波峰和波谷,则无需太担心调整Step。

因为稳定的业务量在很长一段时间内基本上是固定在一个步长之间的,但如果有这样一个明显的活动时段,那么步长就必须足够灵活,以适应不同时间段业务量的激增或暴跌。

假设业务QPS为Q,号段长度为L,号段更新周期为T,则Q * T = L。

L的长度一开始是固定的,导致随着Q的增长,T会越来越小。

但该方案的本质要求是希望T是固定的。那么如果L能够与Q呈正相关,那么T就可以接近一个固定值。因此,该方案每次更新号段时,都会根据该号段的周期T和上次更新的号段长度步长来确定下一个号段长度nextStep。

下面是一个简单的算法,旨在说明动态更新的意思:

arduino 复制代码
T < 15min,nextStep = step * 2
15min < T < 30min,nextStep = step
T > 30min,nextStep = step / 2

至此,已经满足了号段消费稳定、趋于一定时间区间的需求。

因为从本质上来说,这个方案虽然在DB层做了一些容错的方案,但是最终ID号段的下发方式还是需要很大程度上依赖于DB。

最后,还是要在数据库的高可用上下功夫。

如果喜欢这篇文章,点赞支持一下,关注我第一时间查看更多内容!

相关推荐
IT_陈寒37 分钟前
Vite项目build后路由404了?你可能漏了这个小配置
前端·人工智能·后端
宸津-代码粉碎机1 小时前
Spring AI企业级实战|从RAG优化到Agent多工具调度
java·大数据·人工智能·后端·python·spring
吴佳浩1 小时前
AI Infra 的真相:Go 没输,rust也不是取代
后端·rust·go
喵个咪1 小时前
实时游戏网络协议深度对比:KCP vs WebRTC vs WebSocket
后端·websocket·webrtc
冰西瓜6001 小时前
深度学习的数学原理(四十二)—— 分布式训练
人工智能·分布式·深度学习
普通网友2 小时前
springboot之集成Elasticsearch
spring boot·后端·elasticsearch
QuZero2 小时前
Guava Cache Deep Dive
java·后端·算法·guava
leeyi2 小时前
SSE 实时推流 —— Token 怎么一个个蹦出来
后端·agent
leeyi2 小时前
ReAct 循环的 50 行 Go 实现,逐行拆解
后端·agent
leeyi2 小时前
HITL:让人类随时叫停 AI,并且能优雅地继续
后端·agent