前几天看面经的时候遇上了这个问题
- 场景题: 如何在上线后统计用户数和最大用户数?
本文分为两部分来讨论这个问题, 并说明各个方式的弊端
- 客户端服务端采用短链接通信
- 基于数据库
- 基于redis
- 客户端服务端采用长连接通信
- 基于连接数统计
因为该问题对B/S架构或C/S架构来说无差异, 因此下文中不区分客户端/浏览器端
(ps: 创作不易, 对您有帮助的话麻烦帮忙点个赞^_^)
客户端服务端采用短链接通信
网上的一般答案都是针对这种场景的.
如何判断用户是否退出
我们来分析一下用户统计时的难点: 我们无法准确的判断出用户的退出行为.
当服务端和客户端采用短链接进行通信时, 这确实是个难点, 我们可以"假设"用户在某个时间后会退出, 记为"退出时间", 然后我们的服务端可以在收到该用户新的请求时更新这个"退出时间", 同时客户端创建一个定时任务, 在某段时间后发送一个空请求来更新"退出时间".
比如某个用户在12:00, 我们记录人数+1, 然后维护一个过期时间为12:05, 之后服务端每次收到该用户的请求时都更新过期时间为当前时间+5min, 然后客户端定时任务每4min发送一个空请求.
基于数据库
基于上述思路我们可以如下实现
- 数据库中的user表维护一个"过期时间".
- 服务端每收到一个新的用户请求都更新这个过期时间.
- 客户端定时向服务端发送空请求更新过期时间.
- 统计时去user表中查询所有过期时间大于当前时间的行数目.
优点:
- 数据库容量极大, 不会受到容量限制
缺点:
- 存在大量的update操作, 导致了极低的性能
- 统计的数目不是很精确
- 查询数据量大, 导致查询效率低
- 对于未登录用户, 我们只能基于对方的ip地址等信息来进行统计, 统计相对困难
而且我们可以分析下1, 3两条缺点, 他们是无法解决的, 因为优化大数据查询需要索引, 而建立索引又会导致update操作耗时的增加.
基于redis
基于redis的思路和基于数据库的差不多, 只不过我们把存储方式改为了基于内存, 且不再存储全部数据, 从而解决了基于数据库方式的1,3两条缺点.
我们可以如下实现
- redis中维护一个ZSet来存储所有在线用户, 其中key是用户id, value任意, score是"退出时间"
- 服务端每收到一个新的用户请求都向ZSet中ZAdd当前用户, 如果用户之前不存在, 那么ZSet容量会+1, 否则更新其score
- 服务器维护一个定时任务, 每次清除掉 "退出时间" 小于 "当前时间" 的kv
- 客户端定时向服务端发送空请求更新过期时间.
- 统计时统计ZSet中元素个数即可
此外, 我们也可以把ZSet换成bitMap数据结构, 思路一致.
优点:
- 读写速率相对较快
缺点
- redis容量相对较小, 极大用户量时可能存在容量不足问题
- 用ZSet/bitMap来存储所有在线用户, 较大用户量时一定会导致大key问题
- 统计相对不精确
- 对于未登录用户统计困难.
客户端服务端采用长连接通信
这是我个人认为最好的一种统计方式了, 我们可以通过监听API层的长连接数目来判断在线用户数.
如何感知用户退出
当我们采用这种方式时, 非常容易监听到用户退出行为. 因为无论用户通过何种方式关闭客户端, OS都会发起四次挥手来关闭客户端所持有的全部连接.
唯一可能造成误判的情况就是用户主机宕机, 但这种情况相比起预估"退出时间"方式导致的误差要小太多了.
实现
因此, 当我们需要查询在线用户数时, 只需要统计API层所有主机中, 处于"ESTAB状态"且"服务端主机作为目的主机"且"端口是我们的程序监听的端口"的连接数.
我们可以通过如下命令来做到这一点
ss -antp | grep ESTAB | awk '{print $4}' | grep <ip:port> | wc -l
其中
ss -antp | grep ESTAB
用于过滤处于ESTAB状态的长连接
awk '{print $4}
用来查询这些连接socket四元组中的目的ip:port
grep <ip:port>
用来过滤出我们程序监听的ip:port(如果程序所在主机只有一个ip地址, 那么只需要过滤:port即可)
最后通过wc -l命令来完成计数
优点:
- 统计相对精确
- 不存在直接修改操作, 查询效率高
- 不消耗额外内存/磁盘
- 便于统计未登录用户
缺点:
- 对于同一个用户在不同终端进行的登录难以区分
- 依赖于长连接