关于用 Redis 做在线人数统计

2016-08-12 17:08:58 +08:00
 Livid

在线统计是很多 SNS 的常见功能。最近在优化 V2EX 的过程中,实现了一种新的方式,性能不错,分享给大家。

  1. 使用一个单独的 Redis 数据库
  2. 每个在线用户是一条带有 TTL 的记录,在每次 Session 开始时写入这条记录到 Redis
  3. 需要统计当前有多少人在线的话,只需要在这个数据库上用 dbsize() 就可以获得,不会遇到 keys() 可能带来的性能问题
23231 次点击
所在节点    Redis
33 条回复
est
2016-08-13 00:17:44 +08:00
@Livid @julyclyde

假设 NOW() 得到当前 UNIX TIMESTAMP 时间戳, EXPIRE 是 session 过期时间,有如下方法:

某用户 uid 上线了,刷新 session :

zadd("ONLINE_USERS", uid, NOW() + EXPIRE)

得到在线用户数:

方法 1 :
zcount("ONLINE_USERS", NOW(), 9999999999) # 时间复杂度: O(log(N))
zremrangebyscore("ONLINE_USERS", 0, NOW()) # 时间复杂度: O(log(N)+M),可以异步,或离线进行

方法 2 :

zremrangebyscore("ONLINE_USERS", 0, NOW()) # 时间复杂度: O(log(N)+M)。可以定时任务执行。
zcard("ONLINE_USERS") # 时间复杂度: O(1)

@livid 那个方法是 O(N),
@Jaylee 的 setbit ,如果没有用 CPU 指令 __POPCNT 或者 SSE2 加速,那么效率也一般。
fork3rt
2016-08-13 07:40:00 +08:00
TTL 过期时间是多少呢?
wujunze
2016-08-13 10:19:45 +08:00
思路不错 可以试试
nowcoder
2016-08-13 12:21:24 +08:00
@julyclyde 请问 zset 是什么性能问题?
huangz
2016-08-13 13:19:19 +08:00
Livid 的这个思路很有趣!

最近我也在研究这方面的问题,提供一些备选方案让大家参考。


方案 1 :使用有序集合
--------------------------------------

每当用户上线时,执行以下操作:

ZADD("online_users", <user_id>, <current_timestamp>)

想要知道有多少人在指定的时间区间(比如一天或者一周)内上线过,那么可以使用时间区间的起始时间戳和结束时间戳作为参数,调用 ZCOUNT 命令:

ZCOUNT("oneline_users", <start_timestamp>, <end_timestamp>)

判断用户的 session 是否过期可以通过以下方法:

user_online_timestamp = ZSCORE("online_users", <user_id>)
return (user_online_timestamp+SESSION_EXPIRETIME) < now()

其中 SESSION_EXPIRETIME 为 SESSION 的有效期秒数。

这个方案的优点是信息齐全,能够通过有序集合的特性方便地执行区间操作( O(logN)),也可以快速地获取指定用户的登录时间( O(1))。缺点是耗费的内存比较大,并且需要手动删除有序集合中已经过期的用户信息。


方案 2 :使用 HyperLogLog
--------------------------------------

用户上线:

PFADD("online_users", <user_id>)

获取在线人数:

PFCOUNT("online_users")

这个方案的优点是非常节约内存,无论网站的用户数量有多大,一个 HyperLogLog 都只消耗 12 KB 内存。当然,这个方案的缺点也非常明显:

1. 它无法获取用户的具体登录时间。
2. 因为 HyperLogLog 是一个概率算法,所以它无法准确地判断一个用户是否在线。

以上缺点都可以通过增加一个储存用户登录时间的 Hash 来解决,不过这一样一来,需要消耗的内存也会增加。


方案 3 :使用 bitmap
---------------------------------------

上线:

SETBIT("online_users", <user_id>, 1)

检查指定的用户是否上线:

GETBIT("online_users" <user_id>) == 1

统计在线人数:

BITCOUNT("online_users")

这个方案最有趣的地方,就是可以对多个 bitmap 执行聚合计算,从而计算出诸如“有多少个人连续一周都上线了(全勤)”、“这周一共上线了多少个人”、“有多少人今天上线了但是昨天没上线”等问题:

BITOP("AND", "one_week_both_online", "day_1_online", "day_2_online", ..., "day_7_online") # 计算一周都上线的人

BITOP("OR", "one_week_online_total", "day_1_online", ..., "day_7_online") # 计算这周一共有多少人上线

这个方案储存一个用户的在线信息只需要使用一个二进制位,对于用户数为 100 万的网站来说,使用这一方案只需要花费 125 KB 内存,而储存 1000 万的用户信息只需要花费 1.25 MB 。

虽然 bitmap 节约内存的效果不及 HyperLogLog ,但是使用 bitmap 可以准确地判断一个用户是否上线。对于想要尽量节约内存,但又需要准确地知道用户是否在线,又或者需要对用户的在线信息进行聚合计算的应用来说,这个方案是最佳之选。


结语
---------------------------------------

好的,关于统计在线用户的备选方案就介绍到这里,希望这些方案会给大家带来帮助和启发。

最后打个小广告,我正在写一本名为《 Redis 使用教程》的书,里面不仅对用户 SESSION 储存、用户在线统计等问题给出了详细的解法,还提供了实际可运行的 Python 代码,上面给出的一些方案在书中也有介绍,有兴趣的朋友可以关注一下: RedisGuide.com 非常感谢!

huangz
2016.8.13
br00k
2016-08-13 13:58:49 +08:00
@huangz 学习了 ^ ^
changwei
2016-08-13 15:28:51 +08:00
@scott1743 我也有同样的疑问, session 是有过期时间的,为什么不考虑直接统计 session 数据库的 dbsize 呢?
tairan2006
2016-08-13 15:44:15 +08:00
如果要求不精确的话,从 log 端做流数据处理的时候异步统计更简单吧
yangff
2016-08-13 21:00:04 +08:00
@changwei session 过期时间更长
fengjianxinghun
2016-08-13 21:19:19 +08:00
@huangz 不错,期待大作
owt5008137
2016-08-13 21:30:17 +08:00
这个想法很有意思呀
julyclyde
2016-08-15 13:23:28 +08:00
@nowcoder 100w 元素的时候 zrange 慢
codingbody
2021-05-12 23:31:25 +08:00
@huangz 老哥请教一下,有没有什么办法做到,统计 session 持续时间每超过 30min 次数(持续时间在 30min 内记 1 次,60min 内记 2 次,有点类似一个 session 每超过 30min 相当于是一个新的 session,只是 session id 没有变),目的是为了计费使用。

这里有详细一点的描述: https://v2ex.com/t/776514

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/298920

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX