近两年不知道大家有没有发现,现在的面试中《场景题》问的越来越多了,一方面是就业市场竞争者较多所带来的必然结果 ;另一方面是随着时间的推移,公司对于应聘者的技术要求也越来越高了,这时候只会八股文就不够了,你还得会更难的场景题才行。
所以,今天我们就来盘 Java 中的常见面试题《如何实现亿级用户在线状态统计?》,这个时候有人就会说了:"亿级?你确定你们公司有亿级用户同时在线的场景?""我会亿级系统的设计还会来你们公司应聘吗?可笑"。
哈哈哈,确实如此,这些质疑都是合理的。但是话说回来,面试的难度本来就比实际工作的难度大很多;其次,你来应聘是想拿到高薪的 Offer,而不是和面试官干仗来的,对吧?所以,搞明白这道题的答案才是我们关注的重点。
1.亿级用户在线场景分析
例如,QQ 在线状态的统计功能就是亿级的,它的特征是:数据量大、内存占用高、实时性要求高,因此我们使用常规的解决方案是不能实现的。例如,在数据库中给每个用户中添加一个在线状态,上线设为 1,下线设为 0,通过统计状态为 1 的数据,获取在线人数。该方案无法承受大规模用户频繁上、下线操作,会给数据库带来巨大 IO 压力,且实时统计需不断刷新查询,易拖垮数据库性能,因此不可取。
2.解决方案
此时,我们的统计实现可分为以下两类:
- 基于总数的统计方案 :设置一个总在线人数,上线 +1、下线 -1,从而实现上线总人数的统计。
- 优点:实现简单、效率高、内存占用少。
- 缺点:不精准,没办法精确的查找某些用户某个时刻的在线状态;且在异常退出应用的情况下,后续基于在线监测机制的重复下线判断很难实现。
- 基于具体用户详情的统计方案 :将用户的标识(如 QQ 号)和上线状态都存储在集合中。
- 优点:统计精准,可以查找某些用户某个时刻的在线状态;且在异常退出应用的情况下,后续基于在线监测机制可以精准的实现下线用户的去重功能。
- 缺点:内存占用大、效率较低。
3.具体实现
3.1 基于总数的统计方案
基于总数的统计,我们可以使用以下两种方式:
- 基于 Redis 的 incr(+1)和 decr(-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、设计模式、消息队列等模块。