找回密码
 会员注册
查看: 25|回复: 0

Redis定长队列的探索和实践

[复制链接]

3

主题

0

回帖

10

积分

新手上路

积分
10
发表于 2024-10-4 21:17:54 | 显示全部楼层 |阅读模式
vivo 互联网服务器团队 - Wang Zhi一、业务背景从技术的角度来说,技术方案的选型都是受限于实际的业务场景,都以解决实际业务场景为目标。在我们的实际业务场景中,需要以游戏的维度收集和上报行为数据,考虑数据的量级,执行尽最大努力交付且允许数据的部分丢弃。数据上报支持游戏的维度的批量上报,支持同一款游戏128个行为进行批量上报。数据上报需要时效控制,上报的数据必须是上报时刻的前3分钟的数据。整体数据的业务形态如下图所示:二、技术选型从业务的角度来说包含数据的收集和数据的上报,我们把数据的收集比作生产者,数据的上报比作消费者,是一个典型的生产消费模型。生产消费模型在JVM进程内部通过队列+锁或者无锁的Disruptor来实现,在跨进程场景下通过MQ(RocketMQ/kafka)进行处理解耦。但是细化到具体业务场景来看,消息的消费有诸多限制,包括:游戏维度的批量行为上报,行为上报的时效限制,细化到各个技术方案选型进行对比。方案一使用RocketMQ 或者Kafaka等消息队列来存储上报的消息,但是消费侧需要考虑在业务进程中按照游戏维度进行聚合,其中技术细节涉及按照游戏维度进行拆分,在满足消息时效性和批量性的前提下触发上报。在这种方案下消息中间件扮演的角色本质上消息的中转站,没有解决任何业务场景中提及的游戏维度拆分、批量性和时效性。方案二在方案一的基础上,寻求一种技术方案来解决游戏维度的消息分组、批量消费 、时效性。通过Redis的list结构来实现队列(进一步要求实现定长队列)来解决游戏维度的消息分组;通过Redis的list支持的Lrange来实现批量消费;通过业务侧的多线程来解决时效问题,针对高频的游戏使用单独的线程池进行处理,上述两个手段能够保证消费速度大于生产速度。方案对比对比两种方案后决定使用Redis的实现了一个伪消息中间件:通过List对象实现定长队列来保存游戏维度的行为消息(以游戏作为key的List对象来保存用户行为);通过List来保存所有的存在行为数据的游戏列表;通过Set来进行去重判断来保证2中的List对象的唯一性。整体的技术方案如下图所示:生产过程步骤一:游戏维度的某行为数据PUSH到游戏维度的队列当中。步骤二:判断游戏是否在游戏的集合Set中,如果在就直接返回,如果不在进行步骤三。步骤三:往游戏列表中PUSH游戏。消费过程步骤一:从游戏对象的列表中循环取出一款游戏。步骤二:通过步骤一获取的游戏对象去该游戏对象的行为数据队列中批量获取数据处理。三、技术原理在Redis的支持命令中,在List和Set的基础命令,结合Lua脚本来实现整个技术方案。消息数据层面,通过单独的List循环维护待消费的游戏维度的数据,每个游戏维度使用定长的List来保存消息。消息生产过程中,通过结合List的llen+lpop+rpush来实现游戏维度的定长队列,保证队列的长度可控。消息消费过程中,通过结合List的lrange+ltrim来实现游戏维度的消息的批量消费。在整个执行的复杂度层面,需要保证时间复杂度在0(N)常量维度,保证时间可控。3.1 Lua 脚本EVAL script numkeys key [key ...] arg [arg ...] 时间复杂度:取决于脚本本身的执行的时间复杂度。 > eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second1) "key1"2) "key2"3) "first"4) "second" Redis uses the same Lua interpreter to run all the commands.Also Redis guarantees that a script is executed in an atomic way:no other script or Redis command will be executed while a script is being executed.This semantic is similar to the one of MULTI / EXEC.Fromthepointofviewofalltheotherclientstheeffectsofascriptareeitherstillnotvisibleoralreadycompleted.Redis采用相同的Lua解释器去运行所有命令,我们可以保证,脚本的执行是原子性的。作用就类似于加了MULTI/EXEC。Lua 脚本内多个命令以原子性的方式执行,保证了命令执行的线程安全。Lua 脚本结合List命令实现定长队列,实现批量消费。Lua 脚本仅支持单个key的操作,不支持多key的操作。3.2 List 对象LLEN key 计算List的长度 时间复杂度:O(1)。 LPOP key [count] 从List的左侧移除元素 时间复杂度:O(N),N为移除元素的个数。 RPUSH key element [element ...] 从List的右侧保存元素 时间复杂度:O(N),N为保存元素的个数。List的基础命令包括计算List的长度,移除数据,添加数据,整体命令的复杂度都在O(N)的常量时间。整合上述三个命令,我们能保证实现固定长度的队列,通过判断队列长度是否达到定长结合新增队列元素和移除队列元素来完成。LRANGE key start end 时间复杂度:O(S+N), S为偏移量start, N为指定区间内元素的数量。 下标(index)参数 start 和 stop 都以 0 为底,也就是说,以 0 表示列表的第一个元素,以 1 表示列表的第二个元素,以此类推。 你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。 LTRIM key start stop 时间复杂度:O(N) where N is the number of elements to be removed by the operation. 修剪(trim)一个已存在的 list,这样 list 就会只包含指定范围的指定元素。List的基础命令包括批量返回数据和裁剪数据,整体命令的复杂度都在O(N)的常量时间。整合上述两个命令,我们能够批量消费数据并移除队列数据,通过LRANGE批量返回数据并通过LTRIM保留剩余数据。3.3 Set 对象SADD key member [member ...] 往Set集合添加数据。 时间复杂度:O(1)。 SISMEMBER key member 判断Set集合是否存在元素。 时间复杂度:O(1)。通过Set集合来保证数据的唯一性,且时间复杂度可控。四、技术应用4.1 生产消息定义LUA脚本 CACHE_NPPA_EVENT_LUA = "local retVal = 0 " + "local key = KEYS[1] " + "local num = tonumber(ARGV[1]) " + "local val = ARGV[2] " + "local expire = tonumber(ARGV[3]) " + "if (redis.call('llen', key)
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2025-1-12 03:57 , Processed in 0.466784 second(s), 26 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表