vivo 互联网服务器团队 - Wang Zhi

一、业务背景

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

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

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

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

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

二、技术选型

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

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

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

方案一

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

方案二

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

方案对比

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

  1. 通过List对象实现定长队列来保存游戏维度的行为消息(以游戏作为key的List对象来保存用户行为);
  2. 通过List来保存所有的存在行为数据的游戏列表;
  3. 通过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 second
1) "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.
From the point of view of all the other clients the effects of a script are either still not visible or already completed.

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)。

四、技术应用

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) < num) then redis.call('rpush', key, val) " +
"else redis.call('lpop', key) redis.call('rpush', key, val) retVal = 1 end " +
"redis.call('expire', key, expire) return retVal"; 执行LUA脚本
String data = JSON.toJSONString(nppaBehavior);
Long retVal = (Long)jedisClusterTemplate.eval(CACHE_NPPA_EVENT_LUA, 1, NPPA_PREFIX + nppaBehavior.getGamePackage(), String.valueOf(MAX_GAME_EVENT_PER_GAME), data, String.valueOf(NPPA_TTL_MINUTE * 60)); 执行效果
实现固长队列的数据存储并设置过期时间
  • 通过整合llen+rpush+lpop三个命令实现定长队列。
  • 通过lua脚本保证上述命令的原子性执行。

  • 整体的执行流程如上图所示,核心理念通过lua脚本的原子性保证了队列长度计算(llen)、队列数据移除(lpop)、队列数据保存(rpush)的原子性执行。

4.2 消费消息

定义LUA脚本
QUERY_NPPA_EVENT_LUA =
"local data = {} " +
"local key = KEYS[1] " +
"local num = tonumber(ARGV[1]) " +
"data = redis.call('lrange', key, 0, num) redis.call('ltrim', key, num+1, -1) return data"; 执行LUA脚本
Integer batchSize = NppaConfigUtils.getInteger("nppa.report.batch.size", 1);
Object result = jedisClusterTemplate.eval(QUERY_NPPA_EVENT_LUA, 1,NPPA_PREFIX + gamePackage, String.valueOf(batchSize)); 执行效果
取固定数量的对象,然后保留队列的剩余的消息对象。
  • 通过整合lrange+ltrim两个命令实现消息的批量消费。
  • 通过lua脚本保证上述命令的原子性执行。

  • 整体的执行流程如上图所示,核心理念通过lua脚本的原子性保证了数据获取(Lrange)和数据裁剪(Ltrim)的原子性执行。
  • 整体的消费流程选择pull模式,通过多线程循环轮询可消费的队列进行消费。与借助于redis的pub/sub的通知机制实现消费流程的push模式相比,pull模式成本更低效果更佳。

4.3 注意事项

  • Redis集群模式下,执行Lua脚本建议传单key,多key会报重定向错误。
  • 在不同的Redis版本下,Lua脚本针对null的返回值处理不同,参考官方文档。
  • 消费者的消费过程中通过循环遍历游戏列表,然后根据游戏去获取对应的消息对象,但是不同的游戏对应的热度不同,所以在消费端我们通过配置的方式为热门游戏单独开启消费线程进行消费,相当于针对不同游戏配置不同优先级的消费者。

五、线上效果

  • 生产和消费的QPS约为1w qps左右,整体上报QPS通过批量上报后会远低于生产的消息生产和消费的QPS。
  • 整体数据的使用游戏包名作为key进行存储,性能上不存在热点的问题。

六、适用场景

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

七、总结

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

Redis 定长队列的探索和实践的更多相关文章

  1. Redis作为消息队列服务场景应用案例

    NoSQL初探之人人都爱Redis:(3)使用Redis作为消息队列服务场景应用案例   一.消息队列场景简介 “消息”是在两台计算机间传送的数据单位.消息可以非常简单,例如只包含文本字符串:也可以更 ...

  2. 长连接锁服务优化实践 C10K问题 nodejs的内部构造 limits.conf文件修改 sysctl.conf文件修改

    小结: 1. 当文件句柄数目超过 10 之后,epoll 性能将优于 select 和 poll:当文件句柄数目达到 10K 的时候,epoll 已经超过 select 和 poll 两个数量级. 2 ...

  3. Redis深度历险——核心原理与应用实践

    高可用架构」的各位老铁们,你们好!你是否还记得上个月发布的文章中,有两篇深入讲解Redis的文章,分别是和,广大粉丝读者们对这两篇文章整体评价颇高.而我就是这两篇文章的原创作者「老钱」(钱文品),我是 ...

  4. FPGA加速:面向数据中心和云服务的探索和实践

    欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 本文由columneditor 发表于云+社区专栏 作者介绍:章恒--腾讯云FPGA专家,目前在腾讯架构平台部负责FPGA云的研发工作,探索 ...

  5. 从C10K到C10M高性能网络的探索与实践

    在高性能网络的场景下,C10K是一个具有里程碑意义的场景,15年前它给互联网领域带来了非常大的挑战.发展至今,我们已经进入C10M的场景进行网络性能优化. 这期间有怎样的发展和趋势?环绕着各类指标分别 ...

  6. zz深度学习在美团配送 ETA 预估中的探索与实践

    深度学习在美团配送 ETA 预估中的探索与实践 比前一版本有改进:   基泽 周越 显杰 阅读数:32952019 年 4 月 20 日   1. 背景 ETA(Estimated Time of A ...

  7. 深度学习在高德ETA应用的探索与实践

    1.导读 驾车导航是数字地图的核心用户场景,用户在进行导航规划时,高德地图会提供给用户3条路线选择,由用户根据自身情况来决定按照哪条路线行驶. 同时各路线的ETA(estimated time of ...

  8. 国产开源数据库:腾讯云TBase在分布式HTAP领域的探索与实践

    ​导语 | TBase 是腾讯TEG数据平台团队在开源 PostgreSQL 的基础上研发的企业级分布式 HTAP 数据库系统,可在同一数据库集群中同时为客户提供强一致高并发的分布式在线事务能力以及高 ...

  9. 涂鸦基于OAuth2在开发者平台上的探索与实践

    前言 开发授权(OAuth2)是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密的资料(如照片.视频.联系人列表),而无需将用户名和密码提供给第三方应用. OAuth2允许用户提供一 ...

随机推荐

  1. Sqoop导入数据到mysql数据库报错:ERROR tool.ExportTool: Error during export: Export job failed!(已解决)

    问题描述: Container killed by the ApplicationMaster. Container killed on request. Exit code is 143 Conta ...

  2. 安装Siege到Linux(源码)

    运行环境 系统版本:CentOS Linux release 7.3.1611 (Core) 软件版本:siege-4.0.4 硬件要求:无 安装过程 1.安装依赖 [root@localhost ~ ...

  3. Spring Boot中的微信支付(小程序)

    前言 微信支付是企业级项目中经常使用到的功能,作为后端开发人员,完整地掌握该技术是十分有必要的. logo 一.申请流程和步骤 图1-1 注册微信支付账号 获取微信小程序APPID 获取微信商家的商户 ...

  4. Solon 1.8.0 发布,云原生微服务开发框架

    相对于 Spring Boot 和 Spring Cloud 的项目 启动快 5 - 10 倍 qps 高 2- 3 倍 运行时内存节省 1/3 ~ 1/2 打包可以缩小到 1/2 ~ 1/10(比如 ...

  5. 树莓派使用Docker部署EdgeX(jakarta版本)

    使用Docker部署EdgeX 老师安排我搞边缘计算,搞了很久都没能明白边缘计算是什么,甚至对其兴趣不大,前一阵弄好了lorawan网关,该做网关内部的边缘计算了,发现自己已经慢慢地学了进去,总是想要 ...

  6. GDKOI 2021 Day2 TG 总结

    又是爆炸的一天,炸多了本蒟蒻已经习以为常 但今天比昨天整整高了 40 分!!!!却还是没有 100 今天本蒟蒻本想模仿奆佬的打字速度,结果思路混乱让我无法开始 T1 不是吧怎么是期望 dp ,期望值怎 ...

  7. Eureka入门

    一个Eureka中分为eureka server和eureka client.其中eureka server是作为服务的注册与发现中心.eureka client既可以作为服务的生产者,又可以作为服务 ...

  8. 微信小程序使用 ECharts

    echarts-for-weixin 是 ECharts 官方维护的一个开源项目,提供了一个微信小程序组件(Component),我们可以通过这个组件在微信小程序中使用 ECharts 绘制图表. e ...

  9. flink 流的合并

    flink 流的合并操作 union union只能合并类型相同的数据,合并的结果仍然是DataStream,结果操作与未合并之前一致. public static void main(String[ ...

  10. ssh打通

    打通ssh https://www.cnblogs.com/yolanda-lee/p/4975453.html