场景之在线人数或者粉丝查询实现
直播间在线人数或者粉丝查询
一、主要功能
通常对于一些实时在线业务中,比如直播业务中的主播,希望让主播看到直播间实时在线粉丝数等数据,从而从数据方面提升主播的整体直播体验。
二、简单方案:
最简单的方案就是通过所有在线人数判断与主播是否构成粉丝关系,每个人进入直播间会产生记录,根据用户ID去遍历主播与用户的关系表,判断记录中is_follow关系是否为1,为1用户则为主播粉丝,记录下来,遍历整张表结束则可以统计出在线粉丝人数。
缺点:对于在线粉丝查询这一功能而言,是相对实时更新并且主播端请求频率比较高的操作,如果每次查询根据每个在线的用户再去扫描表,即使是扫描从库也是很耗时,因此是不可取的。而对于主播而言短暂的粉丝数量误差延迟是可以接受的,所以考虑引入redis进行缓存记录。
三、涉及场景
也就是记录在线粉丝功能中涉及到的主要接口
- 1、用户进入直播间:用户进入直播间之后判断是否粉丝,如果是粉丝并可以添加记录
- 2、用户心跳:一般用户和server,server和主播都会维持等间隔几秒发送一次心跳,心跳的主要作用包括获取直播间的一些基础人数,商品,礼物等信息以及维持连接正常等功能,同样基础数据中也包括在线粉丝数据。而对于用户端而言,可以在用户发起心跳的时候,判断是否是主播粉丝,如果是则添加记录。心跳的具体应用可参考:心跳应用
- 3、用户离开房间:用户离开直播间触发判断是否粉丝,如果是粉丝并从记录删除的操作。
- 4、超时断开连接:一般可能由于一些异常原因,比如网络等问题,用户和server的连接可能会被判超时,而server的策略则多数是,定期会清理一些超时连接,在清理超时连接的时候,根据连接的用户是否主播构成粉丝关系,需要从在线粉丝记录中删除。
- 5、主播关闭直播间:清除粉丝在线记录
- 6、主播开播:开启在线粉丝记录
- 7、开播过程中添加关注:粉丝在直播间添加关注,这里根据情况不太需要更新记录,因为有用户心跳更新粉丝关注,用户心跳稳定且频繁,新增关注带来的在线粉丝数量上的短暂延迟可以接受。
- 8、开播过程中取消关注:同添加关注
等等......
四、可选方案
利用redis的不同数据结构记录在线粉丝。
1、采用计数器
如果只考虑粉丝数量,而不考虑具体哪些粉丝,确实可以使用一个简单的计数器。
初始化设置房间粉丝数,其中live_id是直播间id
set "online-fans:live_id" 0
进入房间以及退出房间,判断粉丝关系之后,对计数器自增或者自减
incr "online-fans:live_id"
decr "online-fans:live_id"
incr/decr这类原子操作命令,使得客户端想实现业务就只要redis提供的一行命令实现读取-修改-写回三个操作,这样避免了并发问题。并且对于在线人数或者在线粉丝数而言,也不会涉及到key的值小于0的情况,因此也不用配合lua脚本根据值做具体的逻辑判断。
2、采用有序集合
用户上线时候,判断构成粉丝关系,则采用ZADD,将用户以及在线时间添加集合中,其中live_id是直播间id,用主播id或者直播id区分不同的在线粉丝集合。current_timestamp是进入直播间时间戳。
ZADD "online-fans:live_id" <user_id> <current_timestamp>
通过ZCARD命令查看集合中的数量,也就是在线粉丝个数
ZCARD “online-fans:live_id”
通过ZCOUNT 查看某一时段进入直播间的粉丝。
COUNT "online-fans:live_id" <start_timestamp> <end_timestamp>
3、采用集合
使用有序集合能够同时储存粉丝的id以及上线时间戳, 但如果只想要记录在线的id, 而不想要储存上线时间, 那么也可以使用集合来代替有序集合进行记录。
当进入直播间,判断是否是粉丝, 执行 SADD 命令将它添加到在线记录中当中:
SADD "online-fans:live_id" <user_id>
通过使用 SISMEMBER 命令, 可以检查粉丝是否在直播间:
SISMEMBER "online-fans:live_id" <user_id>
统计在线粉丝数则可以通过执行 SCARD 命令来完成:
SCARD "online-fans:live_id"
与有序集合相同的是,都是集合类型,可以进行一些交集和并集的聚合操作,比如交集判断连续一周都在直播间的粉丝,并集可以查看一周之内出现在直播间的粉丝等数据。
4、采用Bitmap
使用有序集合或者集合能够储存具体的在线用户名单, 但是却在粉丝量在线大的时候需要消耗比较多的内存;
bitmap相对来说既能够获得在线用户名单, 又可以尽量减少内存消耗。Redis 的位图就是一个由二进制位组成的数组, 通过将数组中的每个二进制位与用户 ID 进行一一对应, 使用位图可以去记录每个粉丝是否在线。
当一个用户进入直播间时,判如果是粉丝,使用 SETBIT 命令, 将这个用户对应的二进制位设置为 1
SETBIT "online-fans:live_id <user_id> 1
通过使用 GETBIT 命令去检查一个二进制位的值是否为 1 , 判断粉丝是否在线:
GETBIT "online-fans:live_id" <user_id>
通过 BITCOUNT 命令, 统计出位图中有多少个二进制位被设置成了1,也即是有多少个粉丝直播间在线:
BITCOUNT "online-fans:live_id"
同样由于,bitmap是用0,1表示对应的粉丝是否在线,也可以多个记录的bitmap形成与或非运算,计算多个时段或者多个直播间在线的粉丝数。
五、实际方案
综合实际的情况和要求,采用集合记录在线粉丝人数。
主播和服务端,用户和服务端都会建立一个虚拟的连接,在网络异常设备或者软件出现问题的时候,对于服务端来说是无感知的,因此一方面靠心跳来告诉服务端这个链接的可靠性,也就是主播端和用户端会按照⼀定的时间间隔向服务端发送⼼跳包,服务端在收到后会更新最新续约时间,同时启动⼀个定时脚本,定时脚本会检测主播或者用户最后续约时间和当前的时间戳的时间间隔是否超过设置,如果超过设置,则认为是已经超时,需要断开连接,同时清空在线粉丝缓存。
1、定义redis集合
设置一个集合,具体如下
key = "online-fans:live_id"
value = {user_id1, user_id2, user_id3....}
其中live_id是直播间或者直播场次id,也用主播id定义,集合中记录用户的user_id即可
2、修改对应场景下的操作
具体操作包括
- 1,2进入直播间和用户心跳场景中,通过获取到用户的用户Id,判断是否与主播构成粉丝关系,如果是,则执行SADD添加集合操作。一般认为主播开播不会超过6h,因此集合有效期设置为为每当有新粉丝进入,则更新缓存有效期6h。
心跳添加user_id到集合中
SADD "online-fans:live_id" <user_id>
更新有效期
EXPIRE "online-fans:live_id" 6*60*60
- 3场景用户离开直播间,可以直接执行SREM从粉丝集合删除,由于redis集合移除元素操作的时候,如果元素在集合内则直接移除,不在忽略,因此不需要再判断粉丝关系。
离开直播间
SREM "online-fans:live_id" <user_id>
- 4超时断开连接主播和服务端,用户和服务端都会建立一个虚拟的连接,在网络异常设备或者软件出现问题的时候,对于服务端来说是无感知的,因此一方面靠心跳来告诉服务端这个链接的可靠性,也就是主播端和用户端会按照⼀定的时间间隔向服务端发送⼼跳包,服务端在收到后会更新最新续约时间,同时启动⼀个定时脚本,定时脚本会检测主播或者用户最后续约时间和当前的时间戳的时间间隔是否超过设置,如果超过设置,则认为是已经超时,需要断开连接,同时清空在线粉丝缓存。心跳的具体应用可参考:心跳应用
用户超时
SREM "online-fans:live_id" <user_id>
主播超时
DEL "online-fans:live_id"
- 5场景中,主播主动关闭直播间,则本场直播的实时在线粉丝人数需要清空。删除对应的直播场次的缓存
DEL "online-fans:live_id" <user_id>
UNLINK "online-fans:live_id" <user_id>
具体的redis删除集合的命令有两个,一个是del,一个是unlink,具体的是由于redis在执行命令操作的时候是一般是单线程的,因此如果是当在线粉丝人数过多导致集合很大的时候,业务流程中执行del操作,会有延迟。因此可以采用单开一个协程或者线程去异步非阻塞执行del操作或者直接使用unlink命令直接返回删除结果,让redis单开一个额外的线程去执行删除操作,不阻塞后端流程。
- 6场景开播情况下,不需要主动创建一个空的集合,因为在添加操作的时候如果集合不存在,则会创建。还有点延迟初始化的效果。
- 7,8 直播间内用户转变为粉丝的情况可以不考虑,心跳本身也有几秒一次的短暂定时机制,心跳到达server会更新状态,数据上的短暂延迟可以接受。
具体操作流程图如下
六、大key问题
为了统计实时在线人数或者在线粉丝数的功能,引入了redis中间件。初步考虑使用全局计数器,实现简单,但是功能有限,只能看到一个人数,无法知道具体有哪些人,扩展性不够。因此目前用一个redis集合来做。
1、大主播关播场景:
但是考虑这种情况:某些大主播上线之后,大量的用户或者粉丝涌入直播间,我们保存在线人数的这个集合会很大,可能几千几万都有可能,变成一个大key问题。这时候如果主播下线关播,对应的现在的操作是删除集合,而这个问题其实比较常见,像一些直播平台签约的主播,都会在固定时间段开播,到点关播。
2、大key问题:
大Key的清理对Redis的稳定性有比较大的影响,redis是单线程执行命令,删除操作其实会阻塞其他命令的执行,阻塞期间,其他客户端与redis建立链接请求的命令也不会执行,最终结果可能就是请求重试并且链接耗尽导致异常。
3、解决方案:
- 1、业务低峰期删除,既然大key的清理阻塞其他请求,因此可以放到凌晨qps低的时候去执行,但是也无法解决阻塞问题,并且对于场景来说不适合,业务要求的是关播直接清除缓存。
- 2、用unlink代替del命令,redis新开一个异步线程删除缓存,不会阻塞redis主线程。这也是目前的做法。
- 3、分片+计数器,可以考虑将大的集合做分片处理,将原来的一个大key集合分片到很多小的集合中,同时用单独的计数服务,单独维护内容的被赞总数,节省了逐个调用scard接口的消耗。代价就是实现上需要计算一下缓存分片值。
分片之后的缓存结构如下:
online-fans:live_id_slice1 => [uid1,uid11,uid111...]
online-fans:live_id_slice2 => [uid2,uid22,uid222...]
online-fans:live_id_slice3 => [uid3,uid33,uid333...]
七、查看所有key情况
有时候redis会存放很多的key,而这些key是业务相关的,上述中设置的在线粉丝key为online-fans:live_id
,而redis可能还存放着相同前缀的key,比如online-user
。
1、查询key
使用过程中会存在查询某类key的问题,简单的办法为通过keys命令进行操作查询到所有符合条件匹配的key,但是由于keys的工作机制导致线上一般禁用该指令,keys算法是遍历算法,复杂度是O(n),也就是数据越多,时间消耗越高,keys指令可能会阻塞其他命令执行导致 Redis 服务假死。可以使用scan命令来完成统计。
2、scan代替keys命令查询
- 复杂度虽然也是 O(n),但是它是通过游标分步进行的,不会阻塞线程
- 提供 count 参数,不是结果数量,是redis单次遍历字典槽位数量(约等于)
- 同 keys 一样,它也提供模式匹配功能;
- 服务器不需要为游标保存状态,游标的唯一状态就是 scan 返回给客户端的游标整数;
- 返回的结果可能会有重复,需要客户端去重复,这点非常重要;
- 单次返回的结果是空的并不意味着遍历结束,而要看返回的游标值是否为零
- SCAN命令是增量的循环,每次调用只会返回一小部分的元素。所以不会让redis假死
- SCAN命令返回的是一个游标,从0开始遍历,到0结束遍历
posted on 2022-07-25 00:50 weilanhanf 阅读(1988) 评论(0) 编辑 收藏 举报