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. Vue 中 watch 的一个坑

    开发所用 Vue 版本 2.6.11 子组件 coma 中两个属性: props: { url: { type: String, default: '' }, oriurl:{ type: Strin ...

  2. 【系统】Windows相关软件下载

    Windows相关软件下载 Visual Studio Windows SQL Server https://my.visualstudio.com/Downloads/Featured?mkt=zh ...

  3. monit 配置详解(monitrc)

    monitrc是Monit的主配置文件(控制文件). monitrc的内容主要分为全局(golbal)和服务(services)两个部分. 默认情况下monitrc文件在/etc/monit目录下. ...

  4. c# 简单的滑动图片验证

    普通的验证码对用户使用体验不友好,出现了滑动图片验证的验证方式,用户只要按住滑块完成图片的拼接即可通过验证(这是最简单的方式,滑动轨迹,数据分析,滑行速度 什么的暂没考虑) 主要的实现思路: 1.先从 ...

  5. 计算机网络 - HTTP和HTTPS的区别

    计算机网络 - HTTP和HTTPS的区别 http所有传输的内容都是明文,并且客户端和服务器端都无法验证对方的身份. https具有安全性的ssl加密传输协议,加密采用对称加密. https协议需要 ...

  6. Sec资产管理——SwebUI开源应用解决方案

    产品简介 Sweb Sec是一款资产管理类的开源解决方案,通过SwebUI平台开发,包含资产管理.耗材管理两种管理系统,由部门管理.区域管理.盘点.标签打印.出入库等核心功能组成. 免费获取方案 开源 ...

  7. transforms.py

    from PIL import Image from torchvision import transforms #python的用法-->tensor数据类型 #通过transforms.To ...

  8. 技术分享 | app自动化测试(Android)--高级定位技巧

    原文链接 XPath高级定位技巧 XPath 简介 XPath 的英文全称为:XML Path Language,意旨对 XML 中的元素进行路径定位的一种语言,它可适用 XML 标记语言,Html ...

  9. Improved Security for a Ring-Based Fully Homomorphic Encryption Scheme-2013:解读

    本文记录阅读此论文的笔记 摘要 (1)1996年,HPS三人提出一个格上的高效加密方案,叫做NTRUEncrypt,但是没有安全性证明:之后2011年,SS等人修改此方案,将其安全规约到标准格上的困难 ...

  10. 合宙Air32F103CBT6开发板上手报告

    2022年6月初合宙新上市了 Air32F103 系列 MCU, 市面上 STM32F103 的克隆军队又增加了新的一员. 这次不知道是哪家的贴牌, 分 Air32F103CBT6 和 Air32F1 ...