分布式下的 ID 实现

分布式 —— 全局唯一 ID 实现方案

为什么需要全局唯一 ID?分布式服务架构模式下分库分表的设计,使得多个库或多个表存储相同的业务数据。这种情况根据数据库的自增ID就会产生相同ID的情况,不能保证主键的唯一性

业务系统对 ID 的要求有哪些呢?

  1. 全局唯一性:不能出现重复的ID号,既然是唯一标识,这是最基本的要求
  2. 趋势递增:在 MySQL InnoDB 引擎中使用的是聚集索引,由于多数 RDBMS 使用 B-tree 的数据结构来存储索引数据,在主键的选择上面我们应该尽量使用有序的主键保证写入性能
  3. 单调递增:保证下一个 ID 一定大于上一个 ID,例如事务版本号、IM增量消息、排序等特殊需求
  4. 信息安全:如果 ID 是连续的,恶意用户的扒取工作就非常容易做了,直接按照顺序下载指定URL即可;如果是订单号就更危险了,竞对可以直接知道我们一天的单量。所以在一些应用场景下,会需要 ID 无规则、不规则
  5. 高可用、高性能:如果 ID 生成系统不可用,则所有下游服务都不可用

对于 3、4 而言,需求是互斥的,无法使用同一个方案解决

通常使用号段模式满足 3,雪花算法满足 4

前后端联调的坑(long 传给前端接受的坑):JS Number 采用的 IEEE 754 规范,也就是相当于 Java 的 double;double 最大允许的正整数为 2 53 − 1 2^{53} - 1 2531,long 最大值为 2 63 − 1 2^{63} - 1 2631,所以可能造成数据精度的丢失

号段模式

号段模式:一次性向数据库申请一批连续的 ID 放入 JVM 缓存,当用完时再从数据库中拉取

号段模式的优点:本机单调递增、全局趋势递增

号段模式的问题:

  1. 步长的选取。步长太长,服务重启则整个号段作废;步长太短,频繁访问数据库,数据库压力很大
  2. 并发问题:多服务器同时获取号段,可能分配同一号段
  3. 线程阻塞:当号段消费完时,需要向数据库拉取号段,在此期间服务不可用
  4. 单点故障:号段拉取完全依赖于数据库,当数据库不可用时整个服务不可用

上诉问题优化:

  1. 自适应步长,步长初始值很小,当成功消费完所有补偿后还没达到阈值则加倍拉取,若中途服务重启,则将步长设为初始值
  2. 加锁。悲观锁(数据库行锁),乐观锁(版本号)
  3. 双号段缓存。在号段快用完时,提前异步加载下一个号段
  4. 多数据库实例支持:当其中一个库不可用时,使用另外一个库拉取 id

多实例支持将引入新的问题,可能造成拉取重复 id,如何避免?

引入内步长的概念。如有三台数据库实例,则将内步长置为 3。起始序列号设 A 为 1,B 为 2,C 为 3

则每次向不同实例拉取时,id + 内步长

此时 A 的 增长趋势为 1 4 7 10 …

此时 B 的 增长趋势为 2 5 8 11 …

此时 C 的 增长趋势为 3 6 9 12 …

号段模式业界经典实现:美团 Leaf-segment,滴滴 tinyid

雪花算法

雪花算法组成:1 + 41 + 10 + 12;1 是符号位始终为 0 保证 id 为正数;41 是时间戳具体到毫秒(从 1970 年开始)理论上限为 69 年;10 是所允许机器 ID 数,最大允许 1024 台机器;12 是同一时间点上最大允许生成的 id 数,即每毫秒最多分配 4096 个 ID

雪花算法优化:时间戳从 1970 年开始,白白浪费几十年,可以从实际投入使用时间开始,如 2022 年;雪花算法各个组成都可以按需进行分配

雪花算法的优点:趋势递增,不依赖第三方组件,按需分配 bit

为什么使用雪花算法,而不是使用号段模式?出于安全性考虑,如:通过 id 猜测订单数量。严禁 id 号生成单调递增

为什么使用雪花算法,而不是使用 UUID?出于性能考虑,插入数据库时主键乱序则可能导致页分裂,严重影响插入效率

雪花算法的缺陷:时间回拨;因为雪花算法生成的 ID 是基于时间的,现实世界的时间是单调的,但对于计算机而言却不是

时间回拨:服务器时间突然倒退到之前的时间

造成时间回拨的原因:

  1. 手动改服务器时间(一般不会)
  2. 服务器开启 NTP 服务做时间校准(重点)

什么是 ntp?网络时间协议,用以同步分布式场景下各服务器时间;基于 UDP 协议

为什么需要 ntp,而不是使用服务器自己的时间?长时间允许的服务器会造成不可容忍的误差,所以需要同步时钟

解决时间回拨方案:

  1. 等待时钟同步:如果当前生成的 ID 时间戳小于前一个就不是有效的 ID,需要循环生成直到为有效 ID;此法可能造成长时间的空转,所以需要设置最大容忍阈值
  2. 改用逻辑时间:仅在启动时拿当前时间,当 12 位的序列号满时,将逻辑时间 + 1;此法可能可能会造成重启时生成 ID 重复,因为当请求很大时逻辑时间可能大于当前时间

雪环算法经典业界实现:MongoDB ObjectID,美团 Leaf-snowflake,百度 UidGenerator

Redis incr

Redis 提供的 INCR 命令能够对指定的键进行原子递增操作,每次递增操作都会返回一个唯一的整数。

ID 重复问题:如果 Redis 服务器采用 RDB 方式持久化,当服务器宕机并重启

RDB 是以快照方式保存数据的,在快照生成后到服务器宕机期间的新数据将会丢失。这意味着当 Redis 服务器重新启动时,自增的 ID 可能会从一个旧的值开始,从而导致生成重复的 ID。

解决方案:使用 AOF 持久化,并配置为每次更新同步(appendfsync always),但这会影响性能。

当主从复制时,增量复制使用的是 replication backlog,所以主节点宕机切换也会数据丢失,从而导致生成重复的 ID

重启时长问题:采用 AOF 持久化虽然可以解决 ID 重复问题,但在 Redis 宕机后重启时,加载 AOF 文件可能会花费很长时间,尤其是 AOF 文件很大的时候。

AOF 文件记录了每一次写操作,重启时需要重新执行这些操作来恢复数据,因此文件越大,重启时间越长。

解决方案:可以使用混合持久化(Mixed Persistence),结合 RDB 和 AOF 的优点。在这种模式下,Redis 定期创建 RDB 快照,并在快照之间记录 AOF。这种方法可以在提高重启速度的同时减少数据丢失。

参考资料

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值