首先说明,本文提到的直播间实际相当于一个特殊的聊天室,只支持文字,图片,语音之类。
公司某个版本的时候,产品提出要做个专家直播间,请一些有名气的专家或者明星之类的来做些交流,提高社区的用户活跃度。这个版本的需求提出到上线只有一个月时间,而且这个版本的需求量不小,关键的是我们leader还出去度假,整个版本后台的设计开发跟进都由我去负责。
接到这个需求的时候,初始以为简单的很,想用现有im(当时使用的是钉钉的)的群来做这个直播间。而一分析发现捷径被堵死了,因为im的大群最多支持5k人,而这个直播间的在线人数最多可能有几十万(我们app用户量是几千万,每天活跃用户几百万),产品叫嚣要百万级,用群之类的肯定不现实了,这就要求自己去重新实现一套。
最初想到的实现方案是长连接的消息推送,这样会有两种选择:1.直接tcp层通讯(自定义或已有消息协议,消息处理,连接处理等都由自己维护)2.websockek。
直接tcp层通讯性能肯定最好,但时间来不及,我个人倾向于websocket(spring4 mvc已经支持,入门简单)。虽然websocket需要维护连接,但复杂性已经降低很多。
可领导认为websocket协议过重(因为是基于http的),想要直接tcp层自己去处理,时间肯定不够,最后讨论结果是这个版本让客户端使用http来主动拉取,下个版本支持tcp的长连接。
技术方案就这么愉快的决定了:由客户端循环的来拉取。实际上,直播间的这个方案后来一直没有变化,支持用户最多到了几十万,而之前提到的tcp长连接,再也没有人提过。
这里要简单说下直播间的业务,直播间不会一直存在,专家只会在线不到两个小时(晚上8:00-9:30)。整个直播间的寿命也不过几天,请求高峰期就是专家在线的那段时间,所以使用http循环垃取,对服务器的压力不是很大。
技术方案确定了,就是做设计的时候了,设计分两部分:1服务器的业务实现,2提供给客户端接口的设计。
第二点简单的很,无非是进入直播间,退出直播间(这两个接口是为了统计之类),发送消息和拉取消息(拉取全部消息和拉取专家消息)。
但业务设计就复杂了。复杂点如下 :
首先,用户和专家的头像及昵称获取问题,查询过于频繁。
其次,我们web端是无状态的,用token来验证用户请求的合法性,这里的请求很大,不能走正常的验证逻辑,此外还有禁言和封禁逻辑。
最后,也是最麻烦的是信息的读取,请求量很大 ,别说直接查询数据库,查询redis缓存也会有很大压力(包括网络的)。
之前说过,任务多时间紧,而且要经常做些开发设计的工作(任务跟进,需求讨论等),所以这个功能必须能快速的完成设计与开发,并且不能依赖太多的额外资源。整个系统的设计是在周五晚上坐公交回家,及周末两天思考后敲定的。
先说下头像及昵称问题,在用户进入直播间的时间(之前提到的接口),会加载用户的信息缓存。有一个单独的用户信息管理类来保存这些信息,每一个直播间对应一个,这是本地的缓存。此外,以直播间的id作为redis的map类型的key,用户id作为map里面的key,value是用户信息的json。每次查询都会先查询本地,查询不到在查询redis,最后走数据库。
接着,说下验证的问题,每个直播间开启的时候会生成一个随机串作为key保存在本地和redis(防止重启后各个服务器的key不一致),用户在进入直播的时候会用这个唯一key对用户uid加密,返回给客户端,这样客户端在后边的每次请求只验证这个加密串,只用本地缓存的key就可以,至于禁言和封禁的信息也会保存在上边说到的用户信息管理类中,直播间的加密key和用户封禁及禁言信息,肯定需要各个服务器保持一致,这个方法下边说到。
最后,说下消息的问题,消息里除了信息外,肯定要有发言人的昵称和头像,或者还要包含引用的消息。在数据库设计的时候,直接做了数据的冗余,保存一条数据包含了所有需要的信息。这个只是解决了一条信息需要查询多次的问题,关于请求量大的问题解决方法也很简单:本地缓存,但各个服务器之间怎么保证数据的一致和完整,才是需要解决的核心,很多人肯定想到了使用订阅模式,由于时间的限制,不好引入新的jms框架,我借助了redis的pub/sub实现了信息的同步,包括上边说到的一些需要保持一致的信息。
正常来说用户看到的和主动拉取的肯定是最新的信息,所以本地只需要缓存5k条的数据,已足够满足正常的接口需要。
当用户发送消息的时候,先插入数据库,然后广播这条信息,各个服务器收到后,保存(如果是专家的要保存一份到专家数据)并做排序。
当用户垃取信息的时候,从本地读取,只有满足一定条件的才会查询数据库(直播中的情况下很少会满足条件),这样几乎全部的请求都会拦截在服务器,只在内存中查询和操作。这样做的好处是,如果服务器扛不住压力,可以横向的去加服务器,redis中保存的数据会保证用户及直播间数据的一致,消息从数据库垃取也只需要一次。
这里有一个需要注意的地方,就是本地信息的缓存,最初使用的是CopyOnWriteArrayList,但这个线程安全的list每次操作的时候是使用复制的模式,而且不支持排序。而我每次查询也需要复制这个list,截取排序后返回。使用CopyOnWriteArrayList没有带来任何好处,而且为了排序带来了额外的开销,所以最终选择了普通的ArrayList,由自己去分段的加锁控制。
直播间上线到现在有半年多的时间了,最高在线用户有几十万,线上也没有出现过什么问题,(循环的机制肯定会有数据拉取的延迟),只是第一次上线的时候网络并发突高导致停顿。今天有时间就记录下自己的开发过程,后续会继续记录自己技术的成长。