实时统计在线用户数的几种方案分析
我们知道在分布式系统中,一个后端服务可能被分开部署了多台服务器。想要统计实时的在线人数,需要借助一个中间件,我这里用的是redis。
这里必须说明一点,很多情况下,用户是不会手动点击登出按钮的,所以我们无法拿到一个非常精确的实时在线的一个数据,只能拿到一个近似实时的一个值。
本文就会给出几种设计方案,来分析下各个方案的优缺点:
| 分案 | 概述 | 优缺点 |
|---|---|---|
| 使用有序集合 | 这种方案能够同时储存在线的用户 和 用户上线时间,能够执行非常多的聚合计算,但是所消耗的内存也是非常可观的。 | |
| 使用集合 | 这种方案能储存在线的用户,也能够执行一定的聚合计算,相对有序集合,所消耗的内存要小些,但是随着用户量的增多,消耗内存空间也处于增加状态 | |
| 使用hyperloglog | 这种方案无论统计多少在线用户, 消耗的内存都是12k,但是只能给出在线用户的统计信息,无法获取准确的在线用户名单 | |
| 使用bitmap | 这种方案还是比较好的,在尽可能节省内存空间情况下,记录在线用户的情况,而且能做一定的聚合运算 |
1、使用有序集合
每当一个用户上线时, 我们就执行 ZADD 命令, 将这个用户的ID 以及它的在线时间添加到指定的有序集合中:
ZADD "online_users" <user_id> <current_timestamp>
通过执行 ZCARD 命令, 我们可以知道总共有多用户在线:
ZCARD "online_users"
使用有序集合储存在线用户的强大之处在于, 它是本文介绍的所有方案当中, 能够执行最多聚合操作的一个方案, 原因在于, 这一方案既可以通过有序集合的成员(也即是用户的 ID)进行聚合操作, 也可以根据有序集合的分值(也即是用户的登录时间)进行聚合操作。
3、使用bitmap
BitMap 原本的含义是用一个比特位来映射某个元素的状态。由于一个比特位只能表示 0 和 1 两种状态,所以 BitMap 能映射的状态有限,但是使用比特位的优势是能大量的节省内存空间。
在 Redis 中,可以把 Bitmaps 想象成一个以比特位为单位的数组,数组的每个单元只能存储0和1,数组的下标在 Bitmaps 中叫做偏移量。

需要注意的是:BitMap 在 Redis 中并不是一个新的数据类型,其底层是基于 Redis 的字符串类型实现的。
offset 参数必须大于或等于 0 ,小于 2^32 (bit 映射被限制在 512 MB 之内)。
BitMap 占用的空间
在弄清 BitMap 到底占用多大的空间之前,我们再来重申下:Redis 其实只支持 5 种数据类型,并没有 BitMap 这种类型,BitMap 底层是基于 Redis 的字符串类型实现的。
我们通过下面的命令来看下 BitMap 占用的空间大小:
# 首先将偏移量是0的位置设为1
127.0.0.1:6379> setbit csx:key:1 0 1
(integer) 0
# 通过STRLEN命令,我们可以看到字符串的长度是1
127.0.0.1:6379> STRLEN csx:key:1
(integer) 1
# 将偏移量是1的位置设置为1
127.0.0.1:6379> setbit csx:key:1 1 1
(integer) 0
# 此时字符串的长度还是为1,以为一个字符串有8个比特位,不需要再开辟新的内存空间
127.0.0.1:6379> STRLEN csx:key:1
(integer) 1
# 将偏移量是8的位置设置成1
127.0.0.1:6379> setbit csx:key:1 8 1
(integer) 0
# 此时字符串的长度编程2,因为一个字节存不下9个比特位,需要再开辟一个字节的空间
127.0.0.1:6379> STRLEN csx:key:1
(integer) 2
通过上面的实验我们可以看出,BitMap 占用的空间,就是底层字符串占用的空间。假如 BitMap 偏移量的最大值是 OFFSET_MAX,那么它底层占用的空间就是:
(OFFSET_MAX/8)+1 = 占用字节数
因为字符串内存只能以字节分配,所以上面的单位是字节。
但是需要注意,Redis 中字符串的最大长度是 512M,所以 BitMap 的 offset 值也是有上限的,其最大值是:
8 * 1024 * 1024 * 512 = 2^32
由于 C语言中字符串的末尾都要存储一位分隔符,所以实际上 BitMap 的 offset 值上限是:
(8 * 1024 * 1024 * 512) -1 = 2^32 - 1
4、使用 HyperLogLog
HyperLogLog 是一种基数估算算法。所谓基数估算,就是估算在一批数据中,不重复元素的个数有多少。
关于 HyperLogLog原理及Redis实现分析 可查阅 贝壳产品技术 的 HyperLogLog原理及Redis实现分析 一文,这里就不赘述了。
Redis 中的 HyperLogLog#
Redis 提供了 PFADD 、 PFCOUNT 和 PFMERGE 三个命令来供用户使用 HyperLogLog。
# 用于向 HyperLogLog 添加元素
# 如果 HyperLogLog 估计的近似基数在 PFADD 命令执行之后出现了变化, 那么命令返回 1 , 否则返回 0
# 如果命令执行时给定的键不存在, 那么程序将先创建一个空的 HyperLogLog 结构, 然后再执行命令
pfadd key value1 [value2 value3]
# PFCOUNT 命令会给出 HyperLogLog 包含的近似基数
# 在计算出基数后, PFCOUNT 会将值存储在 HyperLogLog 中进行缓存,知道下次 PFADD 执行成功前,就都不需要再次进行基数的计算。
pfcount key
# PFMERGE 将多个 HyperLogLog 合并为一个 HyperLogLog , 合并后的 HyperLogLog 的基数接近于所有输入 HyperLogLog 的并集基数。
pfmerge destkey key1 key2 [...keyn]
HyperLogLog 优缺点: 存在一定误差,占用内存少,稳定占用 12k 左右内存,可以统计 2^64 个元素,对于海量数据统计且对精确度要求不高的场景,建议使用。