V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Livid
V2EX  ›  Redis

关于用 Redis 做在线人数统计

  •  1
     
  •   Livid · 2016-08-12 17:08:58 +08:00 · 23561 次点击
    这是一个创建于 3055 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

    1. 使用一个单独的 Redis 数据库
    2. 每个在线用户是一条带有 TTL 的记录,在每次 Session 开始时写入这条记录到 Redis
    3. 需要统计当前有多少人在线的话,只需要在这个数据库上用 dbsize() 就可以获得,不会遇到 keys() 可能带来的性能问题
    33 条回复    2021-05-12 23:31:25 +08:00
    songjiaxin2008
        1
    songjiaxin2008  
       2016-08-12 17:11:55 +08:00
    等于说并不是非常精确的统计(因为 TTL )吗?可能 WebSocket 可以规避这个问题。
    nigelvon
        2
    nigelvon  
       2016-08-12 17:14:05 +08:00
    思路不错~学习了~
    holyghost
        3
    holyghost  
       2016-08-12 17:17:55 +08:00
    思路不错
    qiayue
        4
    qiayue  
       2016-08-12 17:22:19 +08:00
    @songjiaxin2008 在线人数一般都是统计当前这一刻往前一段时间(如 15 分钟或者 30 分钟)的人数
    southwolf
        5
    southwolf  
       2016-08-12 17:23:05 +08:00
    @songjiaxin2008 论坛并不需要特别精确的在线统计,大多数时候都不需要。 websocket 还是太昂贵了
    wy315700
        6
    wy315700  
       2016-08-12 17:35:28 +08:00
    dbsize
    (error) ERR protocol is not supported


    一部分云服务并不支持这条命令
    kn007
        7
    kn007  
       2016-08-12 17:40:49 +08:00
    单纯只做在线统计会不会比较浪费?
    其他功能呢?
    latyas
        8
    latyas  
       2016-08-12 17:57:00 +08:00
    这是用户每次刷新页面都会更新 ttl 的节奏?
    http2
        9
    http2  
       2016-08-12 18:04:35 +08:00
    哈哈,我也用的这个方法。
    KiseXu
        10
    KiseXu  
       2016-08-12 18:09:15 +08:00
    每一次刷新页面,更新 TTL ,这个数据库不但可以统计在线总人数。还可以判断一条记录是否存在来判断用户的在线状态。
    dangyuluo
        11
    dangyuluo  
       2016-08-12 18:11:11 +08:00
    也算是一个新思路。
    scott1743
        12
    scott1743  
       2016-08-12 18:37:59 +08:00
    dbsize 亮了,单独用 redis 的一个数据库来存 session 会不会更方便?
    changshu
        13
    changshu  
       2016-08-12 18:39:17 +08:00   ❤️ 1
    redis 清理 ttl 过期元素不是即时的, dbsize 会偏大一点, 印象里 keys 虽然性能糟糕, 但会清理过期元素.

    感觉 sorted sets 的方案更折中一点.
    est
        14
    est  
       2016-08-12 18:43:54 +08:00   ❤️ 2
    用 sorted set 的 zrange 性能更好。还可以看谁最后上线啥的。
    Livid
        15
    Livid  
    MOD
    OP
       2016-08-12 19:53:40 +08:00
    @est 没有用 sorted set 的原因是需要定期清理,如果不希望里面有太久远的数据的话。
    ooonme
        16
    ooonme  
       2016-08-12 20:34:00 +08:00 via iPhone
    我们是用 web 日志算的,有几分钟延迟还 ok
    workwonder
        17
    workwonder  
       2016-08-12 21:16:44 +08:00 via Android
    elasticsearch 吗?
    Jaylee
        18
    Jaylee  
       2016-08-12 21:56:54 +08:00   ❤️ 1
    正确的做法应该是用 setbit
    julyclyde
        19
    julyclyde  
       2016-08-12 22:00:14 +08:00   ❤️ 1
    @changshu
    @est
    zrange 有性能问题,不但这个 set 会慢,还会拖累整个 redis 实例
    yyfrankyy
        20
    yyfrankyy  
       2016-08-12 22:29:28 +08:00
    只是为了数字的话为何不用 setbit
    est
        21
    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
        22
    fork3rt  
       2016-08-13 07:40:00 +08:00 via iPhone
    TTL 过期时间是多少呢?
    wujunze
        23
    wujunze  
       2016-08-13 10:19:45 +08:00
    思路不错 可以试试
    nowcoder
        24
    nowcoder  
       2016-08-13 12:21:24 +08:00
    @julyclyde 请问 zset 是什么性能问题?
    huangz
        25
    huangz  
       2016-08-13 13:19:19 +08:00   ❤️ 14
    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
        26
    br00k  
       2016-08-13 13:58:49 +08:00
    @huangz 学习了 ^ ^
    changwei
        27
    changwei  
       2016-08-13 15:28:51 +08:00
    @scott1743 我也有同样的疑问, session 是有过期时间的,为什么不考虑直接统计 session 数据库的 dbsize 呢?
    tairan2006
        28
    tairan2006  
       2016-08-13 15:44:15 +08:00
    如果要求不精确的话,从 log 端做流数据处理的时候异步统计更简单吧
    yangff
        29
    yangff  
       2016-08-13 21:00:04 +08:00
    @changwei session 过期时间更长
    fengjianxinghun
        30
    fengjianxinghun  
       2016-08-13 21:19:19 +08:00 via iPhone
    @huangz 不错,期待大作
    owt5008137
        31
    owt5008137  
       2016-08-13 21:30:17 +08:00 via Android
    这个想法很有意思呀
    julyclyde
        32
    julyclyde  
       2016-08-15 13:23:28 +08:00
    @nowcoder 100w 元素的时候 zrange 慢
    codingbody
        33
    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
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5143 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 36ms · UTC 09:39 · PVG 17:39 · LAX 01:39 · JFK 04:39
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.