如何使用 Redis 实现后台房间的数据管理?
摘要:利用 Redis 实现房间业务管理的实践与思考。
文|即构业务后台开发团队
在一些互动场景中,比如语音聊天室、电商直播等,成员控制、连麦、献花、发弹幕等互动功能,通常要求后台服务器能够储存管理房间及房间内成员的数据。
那么如何组织、存储、操作这些数据以完成既定的业务,并且还要同时保证服务器和客户端之间的数据一致性,是实现这类音视频互动场景的业务后台需要考虑的问题之一。
RoomKit 作为即构科技推出的一款全新形态 LCEP(Low-code Engagement Platform)产品,高度抽象了音视频通话、白板涂鸦、文件演示、实时消息等通用能力,模块功能可以任意组装,让用户用低/零码的方式可完成多个业务场景搭建。所以在 Roomkit 这款产品的后台逻辑中,房间数据管理作为业务的核心部分,贯穿了整个开发过程。
Redis 作为一款高性能 kv 数据库,在后台开发中应用十分广泛,Roomkit 后台我们也使用了 Redis 进行房间数据管理。
那么本文我们就来看下,即构后台开发团队在利用 Redis 实现业务时遇到的技术难点和解决方案,读者在使用即构 aPaaS 层实现自己的业务遇到相似的问题时,也可以参考本文进行解决。
一、Roomkit 后台整体介绍
1、功能模块划分
根据业务逻辑,RoomKit 将代码划分为房间控制模块和功能插件模块两大模块。下面为大家详细介绍这些模块的功能。
房间控制模块
房间控制模块主要用于管理房间列表、房间状态、房间内成员的状态交互。
RoomKit 适用的场景多种多样,大班课、直播、小班课、视频会议、1v1,但实际这些场景可以划分为类视频会议场景和类直播场景。
这两个场景的逻辑侧重各不相同,类视频会议场景参与成员相对较少,但是成员之间交互很频繁;而类直播场景参与人数一般较多,但是主播与听众交互相对较少。基于这个标准 RoomKit 后台又将房间控制模块又划分为两个子模块。由于与本文主题无关,这里不再展开描述。
功能插件模块
功能插件模块指的是与 RoomKit 支持的插件功能,如共享、教学插件、IM 等,其逻辑与场景无关,均可作为独立模块进行开发,与房间控制模块的交互通过相互提供 handler 完成。
2、后台服务架构
RoomKit 后台基于 Redis 管理房间数据,并利用即构信令后台提供的即时推送能力,向客户端实时推送房间状态变化通知。下图是后台与其他服务和客户端之间的架构关系。
二、利用 Redis 管理房间数据的关键技术
下面我们主要以【房间控制模块】为例,介绍 Roomkit 后台在利用 Redis 实现房间业务时的一些关键点。
1、利用 Redis 储存房间数据
为了实现房间内的交互功能,Roomkit 后台需要记录房间、成员等状态信息。为了在业务服务器程序之间共享这些数据,我们选择将数据储存在 Redis 中。
Redis 的 hash 结构天然的可以用于记录房间状态等数据,对房间的设置,如开始上课操作,只需要更改对应的 field 即可。
而为了能够跟踪到当前处于打开状态的房间,Roomkit 后台将房间 ID 和创建时间记录到一个全局的 ZSET 中,后台会定时遍历这些房间以处理统计数据、检查离线成员等。
房间内成员状态同样会记录在一个 hash 结构中,而成员 ID 会被记录在与房间 ID 对应的 ZSET中,其中 score 为成员登陆或上次心跳时间,成员每次心跳都会更新 score 到当前时间。
利用 ZSET 按 score 排序的特性,后台可以很容易的筛选出离线的成员并移出房间。
2、Redis key 无过期时间设计
在编写代码的过程中,我们经常会遇到内存泄漏的问题,而在利用 Redis 储存数据的时候,同样也会存在key泄漏的问题。Redis 在作为 cache 中间件使用时,为了避免 key 泄漏,通常都会对 key 设置过期时间。但是在 Roomkit 中,房间数据销毁是在房间结束时发生的,而房间结束时间是由成员控制的,设置过短的 ttl 会导致数据丢失,而设置太长的 ttl 实际上会导致 Redis 的 key 泄漏问题。
针对这个问题,Roomkit 后台采用了无过期时间设计,也就是不对 key 设置 ttl。
为了防止 key 泄漏,在 Roomkit 后台中,每一个动态创建的 key 都会被记录在一些固定的 key 中,在进行销毁的时候,从这些固定的key出发,就可以索引到所有的 key 了。
例如在共享模块中,为了能在成员退出房间时关闭该成员的共享内容,会在成员创建共享内容的同时,创建一个 key 为 personal_share:{uid} 的 SET 结构以记录这个成员创建的共享内容,而这个 key 自身则会被记录到另一个 key 为 share_recycle_bin 的 SET 结构中。
这样在房间结束时,通过获取并删除 cycle_bin 中记录的 key,达到清理数据的目的。
local keys=redis.call("SMEMBERS","share_recycle_bin")
redis.call("DEL",unpack(keys))
另外为了避免要回收的 key 过多导致的 Redis 执行阻塞过长,Roomit 在删除时还做了分批处理的优化。
3、多机协作遍历任务列表
为了实现对每个房间的检查和统计,需要定时遍历房间列表,从中取出需要检查的房间进行业务定义的检查。如果列表很长,仅凭单机完成检查工作需要较长的时间。在单机编程环境下,这类问题我们通常会使用线程池等技术解决。
而在多机环境下,我们会期望将这些任务均衡的摊派到各个服务器上,这就需要各个服务器之间进行协作。
Roomkit 后台利用 Redis 的单线程执行特性和 zscan 机制实现了分布式协作遍历。
简要实现如下:
local now = tonumber(redis.call("TIME")[1])
local cursor = redis.call("GET","cursor_key")
if cursor=="0" then
local next_check_time = redis.call("GET","next_check_time_key")
if tonumber(next_check_time)>now then
return next_check_time-now
end
end
local scan_result = redis.call("ZSCAN","room_zset_key",cursor)
redis.call("SET","cursor_key",scan_result[1])
if scan_result[1]=="0" then redis.call("SET","next_check_time_key",now+scan_interval) end
return scan_result[2]
单个节点执行遍历时,使用 ZSCAN 命令获取一批需要检查的房间号,并将返回的 cursor 值更新到全局 cursor 上。如果 cursor 为 0,表示遍历完毕,这时候会设置下一次开始遍历的时间。在每次执行遍历时会检查这个时间,如果没有到开始时间,则返回。
房间遍历逻辑不与客户端直接交互,且支持水平扩容,实际部署上可以作为独立的功能组件使用,根据业务负载情况动态调整 worker 数量。
4、seq 与最终一致性
在强交互场景下,保持客户端与服务器的数据一致性是非常重要的,否则就会出现状态错乱的情况,影响交互效果。 所以我们采用了 seq 机制来保证数据变动的逻辑顺序。
事实上我们可以把客户端本地所持房间内数据看作是后台所持房间数据的副本,这样客户端(视为 follower ) 和后台 (视为 leader ) 就可以看作一个分布式的数据储存系统。
Roomkit 后台在对数据进行变更后,需要通过信令后台提供的房间内广播能力,向所有的客户端推送变更通知,通知包括变更事件和变更的详细数据,使得客户端与服务器保持一致。
但是客户端所处网络情况是非常复杂的,在弱网情况下可能存在通知丢失、乱序的情况,而通知的丢失和乱序会导致成员和房间状态异常。
由于弱网情况不可避免,为了保证客户端本地所持数据最终能够与后台数据一致,Roomkit 在每条通知上加上了一个用于校验的 seq 号,当后台数据状态发生变更时,seq 都会进行自增,因此 seq 实际上代表了后台数据的版本号。
客户端在首次进入房间时会拉取全量数据及相应的 seq ,然后通过通知里的数据对本地数据进行增量更新,并推进 seq。另外在客户端心跳时,后台也会返回最新的 seq ,客户端在发现本地的 seq 与心跳返回的 seq 不一致时,将再次拉取全量数据来与后台保持数据一致。
利用 seq 还可以避免一些全体操作导致的通知过长问题。
例如在小班课中进行全体闭麦操作,如果将所有人的变更都放到通知中,会因为消息过长而无法发送。因此后台针对这样的全体操作通知进行了优化,仅发送事件本身,不发送变更数据。客户端在接收到通知后,首先核对 seq 是否是本地seq 的下一 seq ,如果是,则认为全体操作作用的数据是与后台一致的,可以在本地进行操作重放,使数据变更到与后台一致,否则就需要拉取全局数据进行覆盖。
5、CAS 操作
竞态条件是在并发编程中最常见的问题,而这在多机环境下也是可能出现的。通常使用 Redis lua 等临界区技术可以避免这个问题,但是如果业务流不能在一个临界区内执行完怎么办?这时我们就需要使用 CAS 操作。
Roomkit 后台在一些复杂逻辑的实现上使用了 Redis 的 lua 脚本机制。但是众所周知,Redis 是单线程执行的,如果 lua 脚本比较复杂,会导致执行时间过长,阻塞其他命令执行。因此Roomkit 后台对部分过长的 lua 进行了拆分,并利用 CAS 避免竞态条件。
例如在演讲模式中,后台会将房间状态 hash 结构中 speaker 字段的值设置为当前主讲人的成员 ID ;如果主讲人直接退出房间,后台在处理通用成员退出逻辑的同时,还要选择一个在线成员设置为主讲人。而选择下一主讲人的逻辑较为复杂,如果放到成员退出逻辑中一起执行,可能会导致执行阻塞;而如果分为两个 lua 脚本执行,则有可能出现这样一种情况:在成员退出脚本执行完毕、设置主讲人脚本开始执行之前,有另外一个设置主讲人的请求到达后端并成功执行,这时候如果再执行设置主讲人脚本,将会覆盖设置主讲人的请求,导致客户端的异常表现。
解决方案一:利用CAS,在执行设置主讲人脚本时,首先查看 speaker 字段的值是否已经改变,如果已经改变则放弃执行。
local speaker=redis.call("HGET","room_stat_key","speaker")
if speaker~=left_speaker_id then return end
-- select next speaker...
这个解决方案仍然有个缺陷:在设置主讲人脚本开始执行之前进程 crash 了,这时候主讲人字段的值将不会改变,这就导致成员已退出房间,但主讲人仍然是该成员这样逻辑不一致的系统状态。
解决方案二:在成员退出脚本中,将 speaker 字段置为初始值也就是空值,代表目前没有主讲人,这样如果遇到进程crash 等情况,虽然无法执行设置主讲人脚本,系统仍然可以保持逻辑一致。
这样设置主讲人脚本逻辑就变更为查看 speaker 字段的值是否为空值,如果不是则放弃执行。
-- user left script
--...
local speaker=redis.call("HSET","room_stat_key","speaker","")
--...
-- set speaker script
--...
local speaker=redis.call("HGET","room_stat_key","speaker")
if speaker~="" then return end
-- select next speaker...
三、结语
本文总结了即构后台开发团队在实现 Roomkit 后台业务时,如何利用 Redis 实现房间管理业务。在分布式环境下保证业务的正确性、保证数据的一致性,是广大后台开发者不懈追求的目标,关于这些问题的思考和实践希望对读者有所帮助。
如何使用 Redis 实现后台房间的数据管理?的更多相关文章
- redis如何后台启动
当安装好redis之后,运行redis-server命令之后,显示如图所示: 但是这样没有办法在这个tab下做任何操作了,因为这个时候使用Ctrl+c之后,就变成了这个样子 然后就关闭了,那么我想让r ...
- protocol error, got 'n' as reply type byte + redis如何后台启动
其它机子的PHP访问redis爆“protocol error, got 'n' as reply type byte ”错误 解决办法: 在redis配置文件redis.conf中注释掉bind配置 ...
- redis 在后台启动
昨天在cmd窗口启动,窗口关闭,再次访问会报错,所以在次打开 首先你要安装服务:redis-server --service-install redis.windows.conf --loglevel ...
- redis服务后台运行
文章目录 进入redis的安装目录 查看目录结构 进入src目录,普通启动效果 编辑redis服务目录下的redis.conf 进入src目录,执行后台运行的命令 检查服务是否开启 进入redis的安 ...
- Linux安装Redis、后台运行、系统自启动
Redis是用C语言编写的开源免费的高性能的分布式内存数据库,基于内存运行并支持持久化的NoSQL数据库. 安装 1)从官网http://download.redis.io/releases/下载re ...
- redis 设置后台守护运行的两种方式
第一种:进入src目录,执行 nohup ./redis-server & 第二种:redis.conf==> daemonize=yes,启动redis-server后面加redis. ...
- windows 下redis在后台运行
打开命令终端,cd进入redis目录 安装redis服务:redis-server --service-install redis.windows.conf --loglevel verbose re ...
- redis前端启动和后台启动的区别
Part I. 直接启动下载官网下载安装tar zxvf redis-2.8.9.tar.gzcd redis-2.8.9#直接make 编译make#可使用root用户执行`make install ...
- centos7安装redis 并配置在后台启动
官网 https://redis.io/download 先进入 目录 /usr/local 1 下载文件包 $ wget http://download.redis.io/releases/red ...
- redis的哨兵集群,redis-cluster
#主从同步redis主从优先1.保证数据安全,主从机器两份数据一主多从2.读写分离,缓解主库压力主redis,可读可写slave身份,只读 缺点1.手动主从切换假如主库挂了,得手动切换master ...
随机推荐
- [oeasy]python0011 - python虚拟机的本质_cpu架构_二进制字节码_汇编语言
程序本质 回忆上次内容 我们把python源文件 词法分析 得到 词流(token stream) 语法分析 得到 抽象语法树(Abstract Syntax Tree) 编译 得到 字节码 (b ...
- .NET科普:.NET简史、.NET Standard以及C#和.NET Framework之间的关系
最近在不少自媒体上看到有关.NET与C#的资讯与评价,感觉大家对.NET与C#还是不太了解,尤其是对2016年6月发布的跨平台.NET Core 1.0,更是知之甚少.在考虑一番之后,还是决定写点东西 ...
- C#枚举高级应用
文章开头先看一道题: 在设计某小型项目的数据库(假设用的是 MySQL)时,如果给用户表(User)添加一个字段(Roles)用来存储用户的角色,你会给这个字段设置什么类型?提示:要考虑到角色在后端开 ...
- 项目中的坑记录~v-if和v-show的坑
有个功能是这样的,点击获取验证码,获取验证码之后将输入框禁用,进行倒计时11秒. 问题:第一次的倒计时是从6开始的, 之后的倒计时都是从9开始倒计,没有从11开始 解决:主要是用了v-show.倒计时 ...
- springsecurity:权限与异常处理
权限即不同用户可以使用不同功能 实现前置: 在上一次登录与校验中,我们将authentication存入到SecurityContextHolder中,后续我们需要从FilterSecurityInt ...
- 2023/4/19 SCRUM个人博客
1.我昨天的任务 初步了解了pandas库,对series和dataframe有了初步的学习使用 2.遇到了什么困难 对PYQT5的概念没有定义,准备进行学习 3.我今天的任务 学习了PYQT5的部分 ...
- vue3 + ts 中出现 类型“typeof import(".........../node_modules/vue/dist/vue")”的参数不能赋给类型“Component<any, any, any, ComputedOptions, MethodOptions>”的参数。
错误示例截图 解决方法 修改shims-vue.d.ts中的内容 declare module "*.vue" { import { defineComponent } from ...
- 压力测试工具httperf使用方法
目录 压力测试工具httperf使用方法 通过tar zxvf解压httperf-0.9.0.tar.gz 进入目录 安装c++编译环境 开始编译 进入编译后的bin目录 开始测试 压力测试工具htt ...
- 【Node】下载安装(Linux)
不要使用源码包安装!!!编译时间太长!! 不要使用源码包安装!!!编译时间太长!! 不要使用源码包安装!!!编译时间太长!! 使用Node源码包安装 这里使用的是源码包安装 Node官网地址:也不是官 ...
- 【Redis】03 Redis 数据类型、相关补充、常用命令
redis的数据类型 1,概述 使用Redis进行应用设计和开发的一个核心概念是数据类型. 与关系数据库不同,在Redis中不存在需要我们担心的表, 在使用Redis进行应用设计和开发时,我们首先应该 ...