Redis 定长队列的探索和实践
时间:2025-11-05 12:18:26 出处:数据库阅读(143)
一、长队业务背景
从技术的探索角度来说,技术方案的和实选型都是受限于实际的业务场景,都以解决实际业务场景为目标。长队
在我们的探索实际业务场景中,需要以游戏的和实维度收集和上报行为数据,考虑数据的长队量级,执行尽最大努力交付且允许数据的探索部分丢弃。
数据上报支持游戏的和实维度的批量上报,支持同一款游戏128个行为进行批量上报。长队
数据上报需要时效控制,探索上报的和实数据必须是上报时刻的前3分钟的数据。
整体数据的长队业务形态如下图所示:

二、技术选型
从业务的探索角度来说包含数据的收集和数据的上报,我们把数据的和实收集比作生产者,源码库数据的上报比作消费者,是一个典型的生产消费模型。
生产消费模型在JVM进程内部通过队列+锁或者无锁的Disruptor来实现,在跨进程场景下通过MQ(RocketMQ/kafka)进行处理解耦。
但是细化到具体业务场景来看,消息的消费有诸多限制,包括: 游戏维度的批量行为上报,行为上报的时效限制,细化到各个技术方案选型进行对比。
方案一使用RocketMQ 或者Kafaka等消息队列来存储上报的消息,但是消费侧需要考虑在业务进程中按照游戏维度进行聚合,其中技术细节涉及按照游戏维度进行拆分,在满足消息时效性和批量性的前提下触发上报。在这种方案下消息中间件扮演的角色本质上消息的中转站, 没有解决任何业务场景中提及的游戏维度拆分、批量性和时效性。
方案二在方案一的基础上,寻求一种技术方案来解决游戏维度的IT技术网 消息分组、批量消费 、时效性 。通过Redis的list结构来实现队列(进一步要求实现定长队列)来解决游戏维度的消息分组;通过Redis的list支持的Lrange来实现批量消费;通过业务侧的多线程来解决时效问题,针对高频的游戏使用单独的线程池进行处理,上述两个手段能够保证消费速度大于生产速度。
方案对比对比两种方案后决定使用Redis的实现了一个伪消息中间件:
通过List对象实现定长队列来保存游戏维度的行为消息(以游戏作为key的List对象来保存用户行为);通过List来保存所有的存在行为数据的游戏列表;通过Set来进行去重判断来保证2中的List对象的唯一性。整体的技术方案如下图所示:

步骤一:游戏维度的某行为数据PUSH到游戏维度的队列当中。
步骤二:判断游戏是否在游戏的集合Set中,如果在就直接返回,如果不在进行步骤三。
步骤三:往游戏列表中PUSH游戏。
消费过程步骤一:从游戏对象的列表中循环取出一款游戏。b2b信息网
步骤二:通过步骤一获取的游戏对象去该游戏对象的行为数据队列中批量获取数据处理。
三、技术原理
在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)。通过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) < 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脚本保证上述命令的原子性执行。
五、线上效果



六、适用场景
在描述完方案的原理和实现细节之后,进一步对适用的业务场景进行下总结。整体方案是基于redis的基本数据结构构建一个伪消息队列,用以解决 消息的单个生产批量消费 的场景,通过多key形式实现消息队列的多Topic模式,重要的是能够借助于redis的原生能力在O(N)的时间复杂度完成批量消费。另外该方案也可以降级作为实现先进先出定长的日志队列。
七、总结
本文主要探索在特定业务场景下通过Redis的原生命令实现类MQ的功能,创新式的通过Lua脚本组合Redis的List的基础命令,实现了消息的分组,消息的定长队列,消息的批量消费功能;整体解决方案在线上环境落地并平稳运行,为特定场景提供了一种通用的解决方案。
猜你喜欢
- 调节电脑错误提示音(优化电脑错误提示音,提高工作效率)
- Mycat核心开发者带你看尽Mycat三大核心配置文件!!
- 什么是MySQL的内存数据加速器——Buffer Pool?
- CISO如何在董事会讨论中提升网络安全意识
- M320显卡(解析M320显卡的关键特点和优势)
- 筑牢数字安全防线 Fortinet防火墙到SASE的安全网络变革
- 超实用 Nginx 极简教程,覆盖了常用场景
- 快、狠、准排查各类系统故障的运维套路
- Ubuntu是一个流行的Linux操作系统,基于Debian发行版和GNOME桌面环境,和其他Linux发行版相比,Ubuntu非常易用,和Windows相容性很好,非常适合Windows用户的迁移,预装了大量常用软件,中文版的功能也较全,支持拼音输入法,预装了Firefox、Open Office、多媒体播放、图像处理等大多数常用软件,一般会自动安装网卡、音效卡等设备的驱动,对于不打游戏不用网银的用户来说,基本上能用的功能都有了,在Windows操作系统下不用分区即可安装使用,就如同安装一个应用软件那么容易,整个Ubuntu操作系统在Windows下就如同一个大文件一样,很容易卸载掉。下面我就介绍一下Ubuntu操作系统安装使用的方法,供Ubuntu新手参考,希望能起到Linux扫盲的作用。 下载Ubuntu Ubuntu有三个版本,分别是桌面版(Desktop Edition),服务器版(Server Edition),上网本版(Netbook Remix),普通桌面电脑使用桌面版即可,下载地址请点这里,32位CPU请选择32bit version,上网本则可下载Netbook Remix,目前Ubuntu已经占据三分之一的上网本市场,仅次于Windows XP系统。Google的Chrome操作系统强有力的对手就是Ubuntu Netbook Remix。 目前最新的版本是9.04版,下载后的文件名是ubuntu-9.04-desktop-i386.iso,大小是698M,通过迅雷下载非常快,大约半个小时左右可以下载完毕。 安装Ubuntu 在Windows下可以不用重新分区,直接像安装一个应用程序那样安装Ubuntu,安装方法是,先使用一个虚拟光驱(例如微软的Windows虚拟光驱)装载ubuntu-9.04-desktop-i386.iso文件,然后运行根目录下的wubi.exe,运行前要将本地磁盘的名字都修改为英文名,否则会出现错误信息“UnicodeEncodeError: ascii codec cant encode characters in position 0-3: ordinal not in range(128)”而无法运行。 运行之后,会出现如下界面,选择“Install inside Windows”即可在Windows下直接安装而无需分区。 接着出现下面的安装界面,选择一个磁盘,然后将语言选择为“Chinese(Simplified)简体中文”,Installation size为Ubuntu环境的总共磁盘大小,然后是登录用户名和密码,设置好了以后就点安装继续。 后面的安装操作很简单,不需要手动干预就可以直接安装好整个操作系统,大部分的硬件驱动都可以自动安装好。提示安装完毕后,重启系统,就可以使用Ubuntu了。 自动登录Ubuntu Ubuntu默认是每次登录都是要输入用户名和密码的,这是基于安全方面的考虑,不过对于桌面版,大家都习惯自己的电脑能自动登录,类似Windows XP系统那样,通过一些设置可以实现Ubuntu自动登录。设置的方法是:点击“系统”—“系统管理”—“登录窗口” (需要输入管理员密码),然后在“安全”选项页—勾选(启用自动登录),然后在下拉列表里选择自己的用户名。之后Ubuntu就能够自动登录了。 开机自动运行程序 类似Windows的启动菜单,在Linux也可以实现开机自动运行一些命令,比较简单的方法是修改 /etc/rc.local 文件,将需要执行的命令添加进去。 桌面设置 Ubuntu的桌面,默认有两个任务栏,一个在上面,一个在下面,通常习惯Windows的用户喜欢将上面的移到下面,Ubuntu的面板无法拖动,在上面点右键后,可以让其显示在屏幕下端。 桌面背景设置和Windows很类似,在“桌面”上点右键,点更改桌面背景,就可以进行修改设置。 修改root密码 Ubuntu默认的用户并不是root,我们可以通过操作来使用root这个超级管理员帐号,以获得更大的权限。先打开终端,然后执行下面的语句 sudo passwd root 就可以修改超级管理员root的密码,之后就可以使用su命令切换到root用户来执行某些更高权限的操作。 Hosts修改 在Windows下,我们上Twitter等网站都需要修改hosts文件,在Linux下也有hosts文件,文件位于/etc/hosts,使用root用户可以编辑修改这个文件,主机名和IP的格式与Windows的完全相同,例如: 127.0.0.1 localhost 在Ubuntu下安装软件 Ubuntu下的软件安装有几种方式,常用的是deb包的安装方式,deb是debian系列的Linux包管理方式,ubuntu属于debian的派生,也默认支持这种软件安装方式,当下载到一个deb格式的软件后,直接在界面上就可以安装。 另一种常见的安装方式是源代码编译安装,很多软件会提供了源代码给最终用户,用户需要自行编译安装,先使用tar将源代码解压缩到一个目录下,然后进入这个目录,执行以下三条命令: ./configure make sudo make install 执行完成后,即可完成软件的编译和安装。 还有一种方式是apt-get的安装方法,APT是Debian及其衍生发行版的软件包管理器,APT可以自动下载,配置,安装二进制或者源代码格式的软件包,因此简化了Unix系统上管理软件的过程。常用的安装命令是: sudo apt-get install 软件名 sudo apt-get remove 软件名 Firefox浏览器的更新 Ubuntu安装完成后会自动安装一个Firefox浏览器,遗憾的是这个Firefox版本通常较低,例如Ubuntu 9.04会安装Firefox 3.0,不过我们可以想办法下载最新的Firefox覆盖掉老版本Firefox,具体方法是,先上Firefox官方网站下载最新的Linux版本Firefox,然后将其解压缩到某一个目录下,例如firefox目录,进入终端,到这个目录的父目录,执行下面的语句: sudo cp -r firefox /usr/lib/firefox-3.5.2 sudo mv /usr/bin/firefox /usr/bin/firefox.old sudo ln -s /usr/lib/firefox-3.5.2/firefox /usr/bin/firefox-3.5.2 sudo ln -s /usr/bin/firefox-3.5.2 /usr/bin/firefox 之后就可以将Firefox成功替换为最新的Firefox 3.52版本,未来的Firefox更新也可以使用这种方法。 Firefox的Flash问题 经过我的实际测试,Ubuntu自动安装的Flash插件swfdec存在很多问题,在Firefox中,很多网页的Flash无法显示,包括Google音乐和开心网等,因此建议使用下面两条语句将其卸载。 sudo apt-get remove swfdec-mozilla sudo apt-get remove swfdec-gnome 之后可安装官方的Adobe Flash Player的Linux版,下载地址是: http://get.adobe.com/flashplayer/ 安装完成后,还要解决中文乱码问题,解决方法是执行下面语句: sudo cp /etc/fonts/conf.d/49-sansserif.conf /etc/fonts/conf.d/49-sansserif.conf.bak sudo rm /etc/fonts/conf.d/49-sansserif.conf 之后,Firefox的Flash就完全正常了,在Firefox中访问开心网等Flash网站,显示都正常。 安装常用软件 介绍完了安装的方法和Firefox,下面就可以去各个网站下载一些常用的Linux软件来安装了,下面是我整理的一些常用的Linux软件列表: Linux QQ:访问这个地址,下载deb文件安装,可以在Linux下玩腾讯QQ。 防火墙 firestarter: 使用 sudo apt-get install firestarter 安装。 杀毒软件 AntiVir: 虽然Linux下的病毒很少,但对于新手还是有必要安装一个杀毒软件,访问这个地址可以下载免费版的AntiVir杀毒软件,这个软件我曾经在《五个最佳的防病毒软件》中介绍过。 rpm 转 deb 工具: 使用 sudo apt-get install alien 安装 JAVA环境安装: JRE的安装 sudo apt-get install sun-java6-jre ,JDK的安装 sudo apt-get install sun-java6-jdk eclipse安装: 先到这个地址下载最新的eclipse,然后使用tar xvfz eclipse-php-galileo-linux-gtk.tar.gz -C /opt 解压缩后就可以使用。 Picasa 3 for Linux安装: 访问这个地址,下载后直接安装。 Google Earth安装: 在这里下载最新版本的Google Earth,下载下来是个BIN文件,在图形界面上右击 GoogleEarthLinux.bin,在“权限”选项卡中勾选“允许以程序执行文件”,如下图。 之后在终端上执行 ./GoogleEarthLinux.bin 即可安装。 安装LAMP环境 Ubuntu的桌面版也可以安装LAMP(Linux + Apache + MySQL + PHP)环境,这里我介绍一个最简单的方法,就是使用XAMPP,这个项目我曾经在《常见的WAMP集成环境》中介绍过,XAMPP不但支持Windows,还支持Linux,在其网站下载之后,运行下面两条命令: tar xvfz xampp-linux-1.7.2.tar.gz -C /opt /opt/lampp/lampp start 就可以启动LAMP环境,XAMPP是功能全面的集成环境,软件包中包含Apache、MySQL、SQLite、PHP、Perl、FileZilla FTP Server、Tomcat等等,很适合开发环境使用。 安装程序添加程序菜单和桌面 有些程序是直接解压缩安装的,因此不会添加“应用程序”的菜单项,我们可以手动将其添加菜单项,具体方法是,打开“系统”—“首选项”—“主菜单”,新增即可。 添加桌面快捷方式是,在桌面上点右键,创建启动器。这个“启动器”就是Windows里面的“快捷方式”。 将“应用程序”的菜单项创建到桌面快捷方式的方法是,在“应用程序”的菜单项上单击鼠标右键,选择“将此启动器添加到桌面”或“将此启动器添加到面板”,就可以了。