这 10 种分布式 ID,太绝了!
今天跟大家一起聊聊分布式 ID 的一些常见方案,希望对你会有所帮助。
1 UUID
UUID (Universally Unique IDentifier) 通用唯一识别码 ,也称为 GUID (Globally Unique IDentifier) 全球唯一标识符。
UUID 是一个长度为 128 位的标志符,能够在时间和空间上确保其唯一性。
UUID 最初应用于 Apollo 网络计算系统,随后在 Open Software Foundation(OSF)的分布式计算环境(DCE)中得到应用。
可让分布式系统可以不借助中心节点,就可以生成唯一标识, 比如唯一的 ID 进行日志记录。
UUID 是基于时间戳、MAC 地址、随机数等多种因素生成,理论上全球范围内几乎不可能重复。
在 Java 中可以通过 UUID 的 randomUUID 方法获取唯一字符串:
运行结果:
优点:UUID 不借助中心节点,可以保持程序的独立性,可以保证程序在不同的数据库之间,做数据迁移,都不受影响。
缺点:UUID 生成的字符串太长,通过索引查询数据的效率比较低。此外,UUID 生成的字符串,顺序没有保证,不是递增的,不满足工作中的有些业务场景。
在分布式日志系统或者分布式链路跟踪系统中,可以使用 UUID 生成唯一标识,用于串联请求的日志。
2 数据库自增 ID
在很多数据库中自增的主键 ID,数据库本身是能够保证唯一的。
MySQL 中的 auto_increment。
Oracle 中 sequence。
我们在业务代码中,不需要做任何处理,这个 ID 的值,是由数据库自动生成的,并且它会保证数据的唯一性。
优点:非常简单,数据查询效率非常高。
缺点:只能保证单表的数据唯一性,如果跨表或者跨数据库,ID 可能会重复。ID 是自增的,生成规则很容易被猜透,有安全风险。ID 是基于数据库生成的,在高并发下,可能会有性能问题。
在一些老系统或者公司的内部管理系统中,可能会用数据库递增 ID 作为分布式 ID 的方案,这些系统的用户并发量一般比较小,数据量也不多。
3 数据库号段模式
在高并发的系统中,频繁访问数据库,会影响系统的性能。
可以对数据库自增 ID 方案做一个优化。
一次生成一定步长的 ID,比如:步长是 1000,每次数据库自增 1000,ID 值从 100001 变成了 101001。
将 100002~101001 这个号段的 1000 个 ID,缓存到服务器的内存从。
当有获取分布式 ID 的请求过来时,先从服务器的内存中获取数据,如果能够获取到,则直接返回。
如果没有获取到,则说明缓存的号段的数据已经被获取完了。
这时需要重新从数据库中获取一次新号段的 ID,缓存到服务器的内存中,这样下次又能直接从内存中获取 ID 了。
优点:实现简单,对数据库的依赖减弱了,可以提升系统的性能。
缺点:ID 是自增的,生成规则很容易被猜透,有安全风险。如果数据库是单节点的,有岩机的风险。
4 数据库的多主模式
为了解决上面单节点岩机问题,我们可以使用数据库的多主模式。
即有多个 master 数据库实例。
在生成 ID 的时候,一个请求只能写入一个 master 实例。
为了保证在不同的 master 实例下 ID 的唯一性,我们需要事先规定好每个 master 下的大的区间,比如:master1 的数据是 10 开头的,master2 的数据是 11 开头的,master3 的数据是 12 开头的。
然后每个 master,还是按照数据库号段模式来处理。
优点:避免了数据库号段模式的单节点岩机风险,提升了系统的稳定性,由于结合使用了号段模式,系统性能也是 OK 的。
缺点:跨多个 master 实例下生成的 ID,可能不是递增的。
5 Redis 生成 ID
除了使用数据库之外,Redis 其实也能产生自增 ID。
我们可以使用 Redis 中的 incr 命令:
给 ID_VALUE 设置了值是 1000,然后使用 INCR 命令,可以每次都加 1。
这个方案跟我们之前讨论过的方案 1(数据库自增 ID)的方案类似。
优点:方案简单,性能比方案 1 更好,避免了跨表或者跨数据库,ID 重复的问题。
缺点:ID 是自增的,生成规则很容易被猜透,有安全风险。并且 Redis 可能也存在单节点,岩机的风险。
6 Zookeeper 生成 ID
Zookeeper 主要通过其 znode 数据版本来生成序列号,可以生成 32 位和 64 位的数据版本号,客户端可以使用这个版本号来作为唯一的序列号。
由于需要高度依赖 Zookeeper,并且是同步调用 API,如果在竞争较大的情况下,需要考虑使用分布式锁。
因此,性能在高并发的分布式环境下,也不太理想。
很少人会使用 Zookeeper 来生成唯一 ID。
7 雪花算法
Snowflake(雪花算法)是 Twitter 开源的分布式 ID 算法。
核心思想:使用一个 64 bit 的 long 型的数字作为全局唯一 id。
最高位是符号位,始终为 0,不可用。
41 位的时间序列,精确到毫秒级,41 位的长度可以使用 69 年。时间位还有一个很重要的作用是可以根据时间进行排序。
10 位的机器标识,10 位的长度最多支持部署 1024 个节点
12 位的计数序列号,序列号即一系列的自增 id,可以支持同一节点同一毫秒生成多个 ID 序号,12 位的计数序列号支持每个节点每毫秒产生 4096 个 ID 序号。
优点:算法简单,在内存中进行,效率高。高并发分布式环境下生成不重复 ID,每秒可生成百万个不重复 ID。基于时间戳,以及同一时间戳下序列号自增,基本保证 ID 有序递增。并且不依赖第三方库或者中间件,稳定性更好。
缺点:依赖服务器时间,服务器时钟回拨时可能会生成重复 ID。
8 Leaf
Leaf 是美团开源的分布式 ID 生成系统,它提供了两种生成 ID 的方式:
Leaf-segment 号段模式
Leaf-snowflake 雪花算法
Leaf-segment 号段模式,需要创建一张表:
这个模式就是我们在第 3 节讲过的数据库号段模式。
biz_tag 用来区分业务,max_id 表示该 biz_tag 目前所被分配的 ID 号段的最大值,step 表示每次分配的号段长度。
原来获取 ID 每次都需要写数据库,现在只需要把 step 设置得足够大,比如 1000。那么只有当 1000 个号被消耗完了之后才会去重新读写一次数据库。
Leaf-snowflake 雪花算法,是在传统雪花算法之上,加上 Zookeeper,做了一点改造:
Leaf-snowflake 服务需要从 Zookeeper 按顺序的获取 workId,会缓存到本地。
如果 Zookeeper 出现异常,Leaf-snowflake 服务会直接获取本地的 workId,它相当于对 Zookeeper 是弱依赖的。
因为这种方案依赖时间,如果机器的时钟发生了回拨,那么就会有可能生成重复的 ID 号,它内部有一套机制解决机器时钟回拨的问题:
9 Tinyid
Tinyid 是滴滴用 Java 开发的一款分布式 id 生成系统,基于数据库号段算法实现。
Tinyid 是在美团的 ID 生成算法 Leaf 的基础上扩展而来,支持数据库多主节点模式,它提供了 REST API 和 JavaClient 两种获取方式,相对来说使用更方便。
但跟美团 Leaf 不同的是,Tinyid 只支持号段一种模式,并不支持 Snowflake 模式。
基于数据库号段模式的简单架构方案:
ID 生成系统向外提供 http 服务,请求经过负载均衡 router,能够路由到其中一台 tinyid-server,这样就能从事先加载好的号段中获取一个 ID 了。
如果号段还没有加载,或者已经用完了,则需要向 db 再申请一个新的可用号段,多台 server 之间因为号段生成算法的原子性,而保证每台 server 上的可用号段不重,从而使 id 生成不重。
但也带来了这些问题:
当 id 用完时需要访问 db 加载新的号段,db 更新也可能存在 version 冲突,此时 id 生成耗时明显增加。
db 是一个单点,虽然 db 可以建设主从等高可用架构,但始终是一个单点。
使用 http 方式获取一个 id,存在网络开销,性能和可用性都不太好。
为了解决这些这些问题:增加了 tinyid-client 本地生成 ID、使用双号段缓存、增加多 db 支持提高服务的稳定性。
最终的架构方案如下:
Tinyid 方案主要做了下面这些优化:
增加 tinyid-client:tinyid-client 向 tinyid-server 发送请求来获取可用号段,之后在本地构建双号段、id 生成,如此 id 生成则变成纯本地操作,性能大大提升。
使用双号段缓存:为了避免在获取新号段的情况下,程序获取唯一 ID 的速度比较慢。Tinyid 中的号段在用到一定程度的时候,就会去异步加载下一个号段,保证内存中始终有可用号段。
增加多 db 支持:每个 DB 都能生成唯一 ID,提高了可用性。
10 UidGenerator
百度 UID-Generator 使用 Java 语言,基于雪花算法实现。
UidGenerator 以组件形式工作在应用项目中, 支持自定义 workerId 位数和初始化策略, 从而适用于 docker 等虚拟化环境下实例自动重启、漂移等场景。
在实现上, UidGenerator 通过借用未来时间来解决 sequence 天然存在的并发限制。
采用 RingBuffer 来缓存已生成的 UID, 并行化 UID 的生产和消费, 同时对 CacheLine 补齐,避免了由 RingBuffer 带来的硬件级「伪共享」问题. 最终单机 QPS 可达 600 万。
Snowflake 算法描述:指定机器 & 同一时刻 & 某一并发序列,是唯一的。据此可生成一个 64 bits 的唯一 ID(long)。默认采用上图字节分配方式:
sign(1bit):固定 1bit 符号标识,即生成的 UID 为正数。
delta seconds (28 bits) :当前时间,相对于时间基点"2016-05-20"的增量值,单位:秒,最多可支持约 8.7 年
worker id (22 bits):机器 id,最多可支持约 420w 次机器启动。内置实现为在启动时由数据库分配,默认分配策略为用后即弃,后续可提供复用策略。
sequence (13 bits):每秒下的并发序列,13 bits 可支持每秒 8192 个并发。
sequence 决定了 UidGenerator 的并发能力,13 bits 的 sequence 可支持 8192/s 的并发,但现实中很有可能不够用,从而诞生了 CachedUidGenerator。
CachedUidGenerator 使用 RingBuffer 缓存生成的 id。RingBuffer 是个环形数组,默认大小为 8192 个(可以通过 boostPower 参数设置大小)。
RingBuffer 环形数组,数组每个元素成为一个 slot。
Tail 指针、Cursor 指针用于环形数组上读写 slot:
Tail 指针:表示 Producer 生产的最大序号(此序号从 0 开始,持续递增)。Tail 不能超过 Cursor,即生产者不能覆盖未消费的 slot。当 Tail 已赶上 curosr,此时可通过 rejectedPutBufferHandler 指定 PutRejectPolicy。
Cursor 指针:表示 Consumer 消费到的最小序号(序号序列与 Producer 序列相同)。Cursor 不能超过 Tail,即不能消费未生产的 slot。当 Cursor 已赶上 tail,此时可通过 rejectedTakeBufferHandler 指定 TakeRejectPolicy。
RingBuffer 填充触发机制:
程序启动时,将 RingBuffer 填充满。
在调用 getUID()方法获取 id 时,如果检测到 RingBuffer 中的剩余 id 个数小于总个数的 50%,将 RingBuffer 填充满。
定时填充(可配置是否使用以及定时任务的周期)。
文章转载自:苏三说技术
评论