基于Redis的序列号服务(分布式id)的设计
前⾔
在笔者团队中,由于分布式 ID 具有单调递增、形成序列的特性,我们习惯将分布式 ID 称为序列号(Sequence),将分布式 ID ⽣产系统为序列号服务系统。因此,本⽂以“序列号”⼀词均指代分布式 ID 来进⾏讲述。
前些天在“开发者头条”的热门分享中有⼀篇携程技术中⼼⼤咖写的 ,⽂章中对多种分布式 ID 系统设计⽅案进⾏了详细的优劣对⽐,并重点讲述了他们最终选择以 flicker ⽅案为基础进⾏优化改进。另外,⽹络上阐述分布式 ID 系统的设计与实现的⽂章数不胜数,其中不少⽂章同样⼲货满满,笔者拜读数遍,受益匪浅,其中包括美团点评 Leaf ,的 seqsvr。
在此,笔者基于团队的业务适⽤性继续对该主题进⾏补充,为读者提供⼀种基于 Redis 的序列号服务系统的设计思路(当时主要是参考的 seqsvr 做的简化⽅案)。
⼀、服务介绍
1.1 概念
分布式 ID ⽣成系统,顾名思义,是在分布式的架构环境中,⽣成全局唯⼀标识的系统。⽐如在常见的业务系统中,分布式 ID 可⽤来标记客户号、订单号、⽂件号、优惠券号等,以保证这些数据的全局唯⼀性。正如前⽂所述,笔者团队所使⽤的分布式 ID 具有单调递增、形成序列的特性(既满⾜全局唯⼀,⼜满⾜排序的特性),被称为序列号,⽽⽣成序列号的系统被称为序列号服务。
序列号服务业内有许多⽅案,⽐如基于 UUID,基于 Redis 的 INCR ⾃增,基于数据库 ID ⾃增,基于 snowflake 的 bit 分段等等,它们各有优缺点和适⽤性。基于团队当前的系统量级与业务适⽤性,笔者团队选择了基于 Redis 的⽅案。
1.2 需求
笔者团队中有些业务场景与⽇切相关,所以将序列号分为两类,⼀类是递增序列(Normal Sequence)、另⼀类是⽇切序列(Batch Sequence)。
永增序列:永远按升序⽣成新序列号
⽇切序列:按⽇切换批次的序列,同⼀天按升序⽣成新序列号,换批次后序列号重置从 1 开始
⼆、服务设计
2.1 设计思路redis五种数据结构
我们知道,序列号服务实现序列号全局唯⼀与单调递增可排序并⾮难事,它的难点在于如何设计好整体架构以满⾜⾼性能,⾼并发以及⾼可⽤等⾮功能特性。
2.1.1 Redis HINCRBY 命令
Redis 的 INCR 命令⽀持 Key 的 “INCR AND GET” 原⼦操作。利⽤这个特性,我们可以在 Redis 中存序列号,让分布式环境中多个取号⽅在集中的 Redis 中通过 INCR 命令来实现取号;同时 Redis 是单进程单线程的架构,不会因为多个取号⽅的 INCR 命令导致取号重复。那么基于 Redis 的 INCR 命令实现序列号的⽣产基本能满⾜全局唯⼀与单调递增的特性,并且性能还不错。
实际上,为了存储序列号的更多相关信息,我们使⽤了 Redis 的 Hash 数据结构,Redis 同样为 Hash 提供 HINCRBY 命令来实
现 “INCR AND GET” 原⼦操作,详情稍后请看 Redis 的数据结构设计。
2.1.2 Redis 宕机序列号恢复问题
我们再想想,Redis 是内存数据库,在提供⾼性能存取的同时,在没有开启 RDB 或者 AOF 持久化的情况下⼀旦宕机序列号将会有丢失。
即便开启了 RDB 持久化,由于最近⼀次快照时间和最新⼀条 HINCRBY 命令的时间有可能存在时间差,宕机后通过 RDB 快照恢复数据集会发⽣取号重复的情况;
⽽ AOF 持久化通过追加写命令到 AOF ⽂件的⽅式记录所有 Redis 服务器的写命令,服务重启后通过执⾏这些写命令恢复数据,理论上数据集都能恢复到最新状态,不会发⽣取号重复的情况;然⽽ AOF 持久化会损耗性能并且在宕机重启后可能由于⽂件过⼤导致恢复数据时间过长;另外,即便能通过 AOF 重写来压缩⽂件,如果是在写 AOF 时发⽣宕机导致⽂件出错,则需要较多时间去⼈为恢复 AOF ⽂件;所以我们需要⼀个恢复⽅案来保证 Redis 序列号服务在 Redis 宕机后可快速恢复数据并且不会导致取号重复。
2.1.3 Redis 宕机序列号恢复⽅案
我们可以利⽤关系型数据库来记录⼀个短时内 最⼤可取序列号 max,取号⽅从 Redis 中取号时只能取⼩于 max 的序列号。另外,我们可以设计两个服务:⼀个定期地统计序列号消费速度,另⼀个定期获取统计值,当 Redis 中 当前可取序列号 cur 取号接近 max 时⾃动更新max 到⼀个适当的值,存⼊数据库和 Redis。在 Redis 宕机的情况下,将从数据库将最⼤可取序列号 max 恢复成 Redis 当前已取序列号cur,防⽌ Redis 取号重复。另外,也有可能关系型数据库发⽣宕机,不过由于主要的取号操作在 Redis,并且设计适当的最⼤可取序列号max 能够提供⾜够时间恢复关系型数据库。
在笔者团队当前的系统量级要求以及业务需求下,这种设计思路经过⼀段时间的⽣产实践相对适⽤,接下来讲述详细的系统设计。
2.2 详细设计
由于⽇切序列在设计上与永增序列差异不⼤,只是多了⼀个⽇期的维度,所以在详细设计的讲述过程中将以永增序列为主,⽇切序列不再赘述。
2.2.1 架构图
从上图可知,序列号服务分两部分:Sequence-Server 和 Sequence-Client,这两部分都依赖于 Redis 和 Mysql。我们先从 Redis 和Mysql 的数据结构设计开始,然后再继续讲述 Server 和 Client 的部分。
2.2.2 Redis 数据结构
A. Sequence Info —— 序列号相关信息
1) 数据结构
2) 数据样例
取号⽅应⽤通过 Sequence-Client 获取序列号的时候,通过 HINCRBY 命令增加 cur 的值并且取出,然后校验当前值是否超出了最⼤可⽤序列号 max。seqs_recently 和 seqs_long_term 记录了 sequenceName 这个序列近期(上⼀秒)和长期(五分钟内平均每秒)消耗的序列号数,Sequence Server ⽤它来计算每次增⼤ max 的步长。
上图显⽰最⼤可⽤序列号 max为 36100,当前已取序列号 cur为 18105,上⼀秒消耗的序列号数 seqs_recently为 0,五分钟内平均每秒消耗的序列号数seqs_long_term为 0。
B. Sequence Stat —— 序列号采样信息
1) 数据结构
2) 数据样例
2.2.3 MySql 数据结构
另外,编写数据库⾃定义函数 —— 更新数据库 最⼤可取序列号 max(其中 last_insert_id(max + step) 为了保证事务),如下:
CREATE FUNCTION `next_max_normal_sequence`(sequence_name varchar (50), step int(11))
RETURNS bigint(20)BEGIN
update normal_sequence set max = last_insert_id(max + step) where name = sequence_name;
return last_insert_id();
END;
2.2.4 Sequence-Server
Sequence-Server 依赖 MySql 数据库⽣成和更新 最⼤可取序列号 max,并开启两个常驻线程把序列号相关信息和统计信息更新到Redis。
A. Sequence Transfer Thread
常驻线程 Sequence Transfer Thread 负责定时(每秒⼀次)通过上⼀秒消耗序列号数 seqs_recently 和近五分钟平均每秒消耗序列号数seqs_long_term,预估下⼀秒消耗的序列号数,从⽽预估未来⼗五分钟将消耗的序列号数。如果当前剩余序列号数不⾜以⽀撑⼗五分钟,则计算未来三⼗分钟将消耗的序列号数作为步长,更新 max 到 MySql 和 Redis,保证取号⽅应⽤每次都能获取到有效的序列号。
B. Sequence Stat Thread
常驻线程 Sequence Stat Thread 负责定时(每秒⼀次)统计取号速率,以便⾃动调整 Mysql 与 Redis 中的 最⼤可取序列号 Max)。2.2.5 Sequence-Client
Sequence-Client 以 jar 包的形式被取号⽅的应⽤所引⽤,它通过封装 “INCR AND GET、校验序列号是否在有效范围” 这两个操作到Lua 脚本中实现原⼦性以及避免多次访问redis造成的性能消耗。
-- Sequence-Client Lua 脚本
local maxSeqNumStr = redis.pcall("HGET", KEYS[1], "max")
if type(maxSeqNumStr) == 'boolean' and maxSeqNumStr == false then
return nil
end
local maxSeqNum = tonumber(maxSeqNumStr)
local seqNum = redis.pcall("HINCRBY", KEYS[1], "cur", 1)
if seqNum <= maxSeqNum then
return seqNum
else
return nil
end
三、服务总结
当前序列号服务⽅案满⾜:
序列号全局唯⼀
⽇切序列单⽇内序列号全局唯⼀
序列号单调递增可排序
⾼并发
可⽤性
Redis(主备) + Mysql
在本机的性能测试如下:
总的来讲,当前我们设计的序列号服务依旧能适⽤于业务需要。随着系统量级的增⼤以及业务需求的变更与演进,序列号服务也会随之做出调整。⽐如 Redis 性能可能成为瓶颈,那么可以在 sequence client 的 HINCRBY 命令上增加⼤于 1 的增量,提供批量获取序列号的功能(需要调整统计序列号消费速率来协助⾃动调整 max 值);也可以为取号⽅提供 Redis 的分⽚功能,不同的取号⽅在各⾃ Redis 中取序列号等等。
⾄此本⽂结束,希望可以为读者提供⼀种基于 Redis 的序列号服务系统的设计思路。当然,其中⽅案的优缺点以及改进点,读者亦可⾃⾏思考总结,到适⽤于⾃⼰的⽅案

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。