共计 4853 个字符,预计需要花费 13 分钟才能阅读完成。
摘要:利用 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 实现房间治理业务。在分布式环境下保障业务的正确性、保证数据的一致性,是宽广后盾开发者不懈谋求的指标,对于这些问题的思考和实际心愿对读者有所帮忙。