关于用 Redis 做在线人数统计 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Livid
186.06D
596.18D
V2EX    Redis

关于用 Redis 做在线人数统计

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

    在线统计是很多 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  
    PRO
       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
    PRO
       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     Solana     2774 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 58ms UTC 14:48 PVG 22:48 LAX 06:48 JFK 09:48
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86