腾讯面试题: 如何统计在线用户数

659 阅读5分钟

前几天看面经的时候遇上了这个问题

  • 场景题: 如何在上线后统计用户数和最大用户数?

本文分为两部分来讨论这个问题, 并说明各个方式的弊端

  • 客户端服务端采用短链接通信
    • 基于数据库
    • 基于redis
  • 客户端服务端采用长连接通信
    • 基于连接数统计

因为该问题对B/S架构或C/S架构来说无差异, 因此下文中不区分客户端/浏览器端

(ps: 创作不易, 对您有帮助的话麻烦帮忙点个赞^_^)

客户端服务端采用短链接通信

网上的一般答案都是针对这种场景的.

如何判断用户是否退出

我们来分析一下用户统计时的难点: 我们无法准确的判断出用户的退出行为.

当服务端和客户端采用短链接进行通信时, 这确实是个难点, 我们可以"假设"用户在某个时间后会退出, 记为"退出时间", 然后我们的服务端可以在收到该用户新的请求时更新这个"退出时间", 同时客户端创建一个定时任务, 在某段时间后发送一个空请求来更新"退出时间".

比如某个用户在12:00, 我们记录人数+1, 然后维护一个过期时间为12:05, 之后服务端每次收到该用户的请求时都更新过期时间为当前时间+5min, 然后客户端定时任务每4min发送一个空请求.

基于数据库

基于上述思路我们可以如下实现

  • 数据库中的user表维护一个"过期时间".
  • 服务端每收到一个新的用户请求都更新这个过期时间.
  • 客户端定时向服务端发送空请求更新过期时间.
  • 统计时去user表中查询所有过期时间大于当前时间的行数目.

优点:

  1. 数据库容量极大, 不会受到容量限制

缺点:

  1. 存在大量的update操作, 导致了极低的性能
  2. 统计的数目不是很精确
  3. 查询数据量大, 导致查询效率低
  4. 对于未登录用户, 我们只能基于对方的ip地址等信息来进行统计, 统计相对困难

而且我们可以分析下1, 3两条缺点, 他们是无法解决的, 因为优化大数据查询需要索引, 而建立索引又会导致update操作耗时的增加.

基于redis

基于redis的思路和基于数据库的差不多, 只不过我们把存储方式改为了基于内存, 且不再存储全部数据, 从而解决了基于数据库方式的1,3两条缺点.

我们可以如下实现

  • redis中维护一个ZSet来存储所有在线用户, 其中key是用户id, value任意, score是"退出时间"
  • 服务端每收到一个新的用户请求都向ZSet中ZAdd当前用户, 如果用户之前不存在, 那么ZSet容量会+1, 否则更新其score
  • 服务器维护一个定时任务, 每次清除掉 "退出时间" 小于 "当前时间" 的kv
  • 客户端定时向服务端发送空请求更新过期时间.
  • 统计时统计ZSet中元素个数即可

此外, 我们也可以把ZSet换成bitMap数据结构, 思路一致.

优点:

  1. 读写速率相对较快

缺点

  1. redis容量相对较小, 极大用户量时可能存在容量不足问题
  2. 用ZSet/bitMap来存储所有在线用户, 较大用户量时一定会导致大key问题
  3. 统计相对不精确
  4. 对于未登录用户统计困难.

客户端服务端采用长连接通信

这是我个人认为最好的一种统计方式了, 我们可以通过监听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命令来完成计数

优点:

  1. 统计相对精确
  2. 不存在直接修改操作, 查询效率高
  3. 不消耗额外内存/磁盘
  4. 便于统计未登录用户

缺点:

  1. 对于同一个用户在不同终端进行的登录难以区分
  2. 依赖于长连接