深入Go底层原理,重写Redis中间件实战|完结

一、业务背景

从技术的角度来说,技术方案的选型都是受限于实际的业务场景,都以解决实际业务场景为目标。

在我们的实际业务场景中,需要以游戏的维度收集和上报行为数据,考虑数据的量级,执行尽最大努力交付且允许数据的部分丢弃。

数据上报支持游戏的维度的批量上报,支持同一款游戏128个行为进行批量上报。

数据上报需要时效控制,上报的数据必须是上报时刻的前3分钟的数据。

整体数据的业务形态如下图所示:

深入Go底层原理,重写Redis中间件实战|完结插图1

二、技术选型

从业务的角度来说包含数据的收集和数据的上报,我们把数据的收集比作生产者,数据的上报比作消费者,是一个典型的生产消费模型。

生产消费模型在JVM进程内部通过队列+锁或者无锁的Disruptor来实现,在跨进程场景下通过MQ(RocketMQ/kafka)进行处理解耦。

但是细化到具体业务场景来看,消息的消费有诸多限制,包括:游戏维度的批量行为上报,行为上报的时效限制,细化到各个技术方案选型进行对比。

方案一

使用RocketMQ 或者Kafaka等消息队列来存储上报的消息,但是消费侧需要考虑在业务进程中按照游戏维度进行聚合,其中技术细节涉及按照游戏维度进行拆分,在满足消息时效性和批量性的前提下触发上报。在这种方案下消息中间件扮演的角色本质上消息的中转站,没有解决任何业务场景中提及的游戏维度拆分、批量性和时效性。

方案二

在方案一的基础上,寻求一种技术方案来解决游戏维度的消息分组、批量消费 、时效性。通过Redis的list结构来实现队列(进一步要求实现定长队列)来解决游戏维度的消息分组;通过Redis的list支持的Lrange来实现批量消费;通过业务侧的多线程来解决时效问题,针对高频的游戏使用单独的线程池进行处理,上述两个手段能够保证消费速度大于生产速度。

方案对比

对比两种方案后决定使用Redis的实现了一个伪消息中间件:

  1. 通过List对象实现定长队列来保存游戏维度的行为消息(以游戏作为key的List对象来保存用户行为);

  2. 通过List来保存所有的存在行为数据的游戏列表;

  3. 通过Set来进行去重判断来保证2中的List对象的唯一性。

整体的技术方案如下图所示:

深入Go底层原理,重写Redis中间件实战|完结插图3

生产过程

步骤一:游戏维度的某行为数据PUSH到游戏维度的队列当中。

步骤二:判断游戏是否在游戏的集合Set中,如果在就直接返回,如果不在进行步骤三。

步骤三:往游戏列表中PUSH游戏。

消费过程

步骤一:从游戏对象的列表中循环取出一款游戏。

步骤二:通过步骤一获取的游戏对象去该游戏对象的行为数据队列中批量获取数据处理。

三、技术原理

在Redis的支持命令中,在List和Set的基础命令,结合Lua脚本来实现整个技术方案。

消息数据层面,通过单独的List循环维护待消费的游戏维度的数据,每个游戏维度使用定长的List来保存消息。

消息生产过程中,通过结合List的llen+lpop+rpush来实现游戏维度的定长队列,保证队列的长度可控。

消息消费过程中,通过结合List的lrange+ltrim来实现游戏维度的消息的批量消费。

在整个执行的复杂度层面,需要保证时间复杂度在0(N)常量维度,保证时间可控。

3.1 Lua 脚本

  • 通过Set集合来保证数据的唯一性,且时间复杂度可控。

四、技术应用

4.1 生产消息

  • 通过整合llen+rpush+lpop三个命令实现定长队列。

  • 通过lua脚本保证上述命令的原子性执行。

深入Go底层原理,重写Redis中间件实战|完结插图5
  • 整体的执行流程如上图所示,核心理念通过lua脚本的原子性保证了队列长度计算(llen)、队列数据移除(lpop)、队列数据保存(rpush)的原子性执行。

4.2 消费消息

深入Go底层原理,重写Redis中间件实战|完结插图7

  • 整体的执行流程如上图所示,核心理念通过lua脚本的原子性保证了数据获取(Lrange)和数据裁剪(Ltrim)的原子性执行。

  • 整体的消费流程选择pull模式,通过多线程循环轮询可消费的队列进行消费。与借助于redis的pub/sub的通知机制实现消费流程的push模式相比,pull模式成本更低效果更佳。

4.3 注意事项

  • Redis集群模式下,执行Lua脚本建议传单key,多key会报重定向错误。

  • 在不同的Redis版本下,Lua脚本针对null的返回值处理不同,参考官方文档。

  • 消费者的消费过程中通过循环遍历游戏列表,然后根据游戏去获取对应的消息对象,但是不同的游戏对应的热度不同,所以在消费端我们通过配置的方式为热门游戏单独开启消费线程进行消费,相当于针对不同游戏配置不同优先级的消费者。

五、线上效果

深入Go底层原理,重写Redis中间件实战|完结插图9

深入Go底层原理,重写Redis中间件实战|完结插图11

深入Go底层原理,重写Redis中间件实战|完结插图13

  • 生产和消费的QPS约为1w qps左右,整体上报QPS通过批量上报后会远低于生产的消息生产和消费的QPS。

  • 整体数据的使用游戏包名作为key进行存储,性能上不存在热点的问题。

六、适用场景

在描述完方案的原理和实现细节之后,进一步对适用的业务场景进行下总结。整体方案是基于redis的基本数据结构构建一个伪消息队列,用以解决消息的单个生产批量消费的场景,通过多key形式实现消息队列的多Topic模式,重要的是能够借助于redis的原生能力在O(N)的时间复杂度完成批量消费。另外该方案也可以降级作为实现先进先出定长的日志队列。

七、总结

本文主要探索在特定业务场景下通过Redis的原生命令实现类MQ的功能,创新式的通过Lua脚本组合Redis的List的基础命令,实现了消息的分组,消息的定长队列,消息的批量消费功能;整体解决方案在线上环境落地并平稳运行,为特定场景提供了一种通用的解决方案。

举报/反馈

原创文章 深入Go底层原理,重写Redis中间件实战|完结,版权所有
如若转载,请注明出处:https://www.itxiaozhan.cn/202210446.html

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注