场景题:如何实现亿级用户在线状态统计?

近两年不知道大家有没有发现,现在的面试中《场景题》问的越来越多了,一方面是就业市场竞争者较多所带来的必然结果 ;另一方面是随着时间的推移,公司对于应聘者的技术要求也越来越高了,这时候只会八股文就不够了,你还得会更难的场景题才行。

所以,今天我们就来盘 Java 中的常见面试题《如何实现亿级用户在线状态统计?》,这个时候有人就会说了:"亿级?你确定你们公司有亿级用户同时在线的场景?""我会亿级系统的设计还会来你们公司应聘吗?可笑"。

哈哈哈,确实如此,这些质疑都是合理的。但是话说回来,面试的难度本来就比实际工作的难度大很多;其次,你来应聘是想拿到高薪的 Offer,而不是和面试官干仗来的,对吧?所以,搞明白这道题的答案才是我们关注的重点。

1.亿级用户在线场景分析

例如,QQ 在线状态的统计功能就是亿级的,它的特征是:数据量大、内存占用高、实时性要求高,因此我们使用常规的解决方案是不能实现的。例如,在数据库中给每个用户中添加一个在线状态,上线设为 1,下线设为 0,通过统计状态为 1 的数据,获取在线人数。该方案无法承受大规模用户频繁上、下线操作,会给数据库带来巨大 IO 压力,且实时统计需不断刷新查询,易拖垮数据库性能,因此不可取。

2.解决方案

此时,我们的统计实现可分为以下两类:

  1. 基于总数的统计方案 :设置一个总在线人数,上线 +1、下线 -1,从而实现上线总人数的统计。
    1. 优点:实现简单、效率高、内存占用少。
    2. 缺点:不精准,没办法精确的查找某些用户某个时刻的在线状态;且在异常退出应用的情况下,后续基于在线监测机制的重复下线判断很难实现。
  2. 基于具体用户详情的统计方案 :将用户的标识(如 QQ 号)和上线状态都存储在集合中。
    1. 优点:统计精准,可以查找某些用户某个时刻的在线状态;且在异常退出应用的情况下,后续基于在线监测机制可以精准的实现下线用户的去重功能。
    2. 缺点:内存占用大、效率较低。

3.具体实现

3.1 基于总数的统计方案

基于总数的统计,我们可以使用以下两种方式:

  1. 基于 Redis 的 incr(+1)和 decr(-1)操作实现,如下图所示:
  1. 基于 Redis 的 HyperLogLog 实现,HyperLogLog (下文简称为 HLL) 是 Redis 2.8.9 版本添加的数据结构,它用于高性能的基数 (去重) 统计功能,它的缺点就是存在极低的误差率(0.81%)。它只需要 12KB 空间就能统计 2^64(约 18 亿)的数据。

此实现方案不能移除元素、存在误差,但空间占用率非常低。

3.2 基于用户的统计实现

基于用户标识(QQ)我们可以采用 Redis 中提供的 Bitmap(位数组)来实现,位数组结构如下:

其中每个下标就可以表示一个具体的数字,例如以上图片标识 1、3 数字存在,如果值为 0 表示不存在,这样的话 **10 亿数字占用的位数组空间位 10 亿 bit,也就是 1000000000/1024/1024/1024/8=0.116GB,可以看出它的空间占用量是非常小的。

用户上线时使用 SetBit 命令将对应位置设为 1 表示在线,下线时设为 0 。判断用户是否在线用 GetBit 命令,统计在线用户数用 BigCount 命令,具体操作命令如下图所示:

在 Spring Boot 项目中,我们可以使用 RedisTemplate 实现用户的上、下线设置,以及在线个数统计,具体实现代码如下:

java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

@Service
public class BitmapService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 设置Bitmap中的位
     * @param key 键
     * @param offset 偏移量
     * @param value 值(0或1)
     */
    public void setBit(String key, long offset, boolean value) {
        redisTemplate.opsForValue().setBit(key, offset, value);
    }

    /**
     * 获取Bitmap中的位
     * @param key 键
     * @param offset 偏移量
     * @return 位的值(0或1)
     */
    public boolean getBit(String key, long offset) {
        return redisTemplate.opsForValue().getBit(key, offset);
    }

    /**
     * 计算Bitmap中值为1的位的数量
     * @param key 键
     * @return 值为1的位的数量
     */
    public Long bitCount(String key) {
        return redisTemplate.opsForValue().bitCount(key);
    }
}

小结

类似这样的场景题我收录和整理的题解答案还有很多,我把它制作成了一个专栏:《Java 常见场景题》,每个场景题都有详细的图解+代码案例解析。学会它可以帮助您解决面试中的大部分场景题,从此不再害怕场景题。查看和解锁所有《Java 常见场景题》扫描以下二维码即可:

本文已收录到我的面试小站 www.javacn.site,其中包含的内容有:场景题、并发编程、MySQL、Redis、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、JVM、设计模式、消息队列等模块。

相关推荐
艾莉丝努力练剑30 分钟前
【AI接入大模型SDK】ChatSDK示例验证
人工智能·学习·面试·大模型·sdk
To_OC5 小时前
跑了3个Docker容器后,我终于搞懂镜像和容器到底啥关系
后端·docker·容器
东风破_6 小时前
Docker 基础:为什么需要容器?
前端·后端·nginx
葡萄成熟时 !6 小时前
JAVA 常用API学习笔记
java·笔记·学习
东风破_6 小时前
Docker 实战:使用 Nginx 作为容器入口代理 Node 服务
前端·后端·nginx
Lyyaoo.6 小时前
Spring Boot Validation 声明式参数校验
spring boot·后端·mysql
剩下了什么6 小时前
float32 与 float64 精度陷阱:如何在 Go 中避免错误使用
开发语言·后端·golang
Rain的Java大神之路7 小时前
介绍一下分布式事务
java·分布式·后端·spring·spring cloud·架构·springcloud
吴声子夜歌7 小时前
Java面试题——基础(一)
java·开发语言
南城以南溫暖如初1478 小时前
外卖CPS实战指南:从技术选型到运营落地全流程解析
java·spring boot·mysql·架构·vue