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

请教诸位一个 redis 统计访问量增速的问题

  •  2
     
  •   waibunleung · 232 天前 · 2219 次点击
    这是一个创建于 232 天前的主题,其中的信息可能已经有所发展或是发生改变。

    自己搞了个小程序,需要统计一下某些内容的访问量增速,比如某个视频 10 分钟内的访问量,在内存有限的情况下(最多 8g),用 redis 来做的话,怎么做比较好?因为存在时间窗口的问题,不知道采用哪种方法比较好。

    关于用户,没登录什么的,所以也不会有什么用户 id 之类的东西

    1. 有序集合,毫秒时间戳为 score,但是如果比较多访问的话,内存占用比较大,实际上我只想要一个数而已
    2. 普通的 key,但是怎么去除 10 分钟前的数据?每分钟存一次访问量数据的话,要统计一个视频的 10 分钟访问量还好,但是要统计多个的时候,就要多次读取 redis
    3. 有没有更好的办法? 成本有限,加内存什么的就先不考虑了吧
    第 1 条附言  ·  232 天前
    这个服务器配置是 8 核 16g,数据库买的阿里云的 RDS 服务,其他的要安装的话,可以提建议,我考虑一下,比如说 mongodb 就可以试一下
    49 条回复    2021-04-23 17:30:33 +08:00
    Jooooooooo
        1
    Jooooooooo   232 天前
    每分钟一个 key 呗.

    key: view_count_{vid}_{minute}

    然后想统计十分钟的就捞十个 key 求和
    opengps
        2
    opengps   232 天前
    视频 id+用途后缀:
    一个计数缓存:来了统计请求就自动加一。每分钟转存到归档
    一个 hash 归档缓存:每分钟存入一份分钟级别统计。
    至于分析,就只从归档 hash 里取值统计,按照分钟 key 去看最近十分钟的结果。统计时候主要依靠 hash 的批量命令操作来减少读取次数
    carity
        3
    carity   232 天前
    直接搞个数据库存呗,这样你想怎么统计都可以了,mysql pgsql 占用内存也不大
    waibunleung
        4
    waibunleung   232 天前
    @Jooooooooo 这就是我提到的第 2 种做法了~
    但问题是,我想做一个管理后台,在视频列表上,显示每个视频的访问增速,如果我一页拿 20 个视频,每个视频捞 10 个 key,20 个视频就要捞 200 个 key,虽然说用 mget 读取也只要读 20 次....但我视频越多,key 就越多了
    waibunleung
        5
    waibunleung   232 天前
    @carity 有数据库,但是每次视频被访问就直接写数据库+1 吗?那到时候数据库写压力就很大了
    liujuzzz
        6
    liujuzzz   232 天前
    i
    waibunleung
        7
    waibunleung   232 天前
    @opengps 每分钟转存到归档,归档指的是 mysql 之类的数据落地的东西?
    另外每分钟转存是后台起一个脚本来做这个吗?
    hash 归档缓存 就相当于只统计了 T-1 分钟的 访问数,这个倒可以接受
    waibunleung
        8
    waibunleung   232 天前
    @liujuzzz 这是插眼的意思吗?
    Jooooooooo
        9
    Jooooooooo   232 天前
    @waibunleung key 多不要紧, 查询做好分页就行.
    ch2
        10
    ch2   232 天前 via iPhone
    你会不会高估了写压力? redis 都不需要,直接 mongo 一个表存就行了,查询也好查询,反正你这个表结构很简单,就视频 id 跟时间戳或者你再加个 ip,这种简单的数据,mongo 存一亿条也无压力
    tianshiyeben
        11
    tianshiyeben   232 天前
    就是监控数据增量变化吧 用 http://www.wgstart.com 有数据监控模块 会定期扫描并生成趋势图表
    tianshiyeben
        12
    tianshiyeben   232 天前
    忘了说了 前提是你得从数据库( mysql pgsql 等关系型数据库)用 sql 能捞到数据
    waibunleung
        13
    waibunleung   232 天前
    @ch2 目前没有配置 mongo,mongo 存一亿条磁盘占用要去到多少呢?不用 redis 纯用 mongo 来承载读写的话,mongo 大概能抗住多大的 qps ?有没有稍微具体一点的数据可以支撑一下?
    seth19960929
        14
    seth19960929   232 天前
    @waibunleung redis 有 pipe, 一次全部捞出来
    waibunleung
        15
    waibunleung   232 天前
    @seth19960929 我知道这个
    dzdh
        16
    dzdh   232 天前
    硬盘允许的话搞个单机的时序数据库哇。
    ch2
        17
    ch2   232 天前
    @waibunleung #13 如果只存我说的那三个字段,大概不到 5G 。写的话 qps 几千肯定没问题的,读取的话你可以用 redis 缓存结果,1 分钟全表查询一次就能得到统计结果
    opengps
        18
    opengps   232 天前
    @waibunleung 我说的归档就是第一个 key 下产生的分钟级数据,存入第二个 key 下当归档数据,然后分析时候使用第二个 key
    vebuqi
        19
    vebuqi   232 天前   ❤️ 3
    遇到过类似的问题,说下解决的方法

    假设我们需要存储视频近 10 分钟内的访问量

    存储方面,我们可以把 10 分钟,分割成 10 个 1 分钟,使用 Redis 的 bitmap 来存储这 10 个量

    在命令方面,redis 的 BITFIELD 命令可以对 bitmap 的多个域同时操作,对每个域支持 GET 、SET 、INCRBY 子命令,可以满足需求

    如图:

    ![image.png]( https://i.loli.net/2021/04/10/LBkiMupGR6ctXoT.png)


    我们把 bitmap 分割成 10 个域,每个域代表 1 分钟的访问量,那么每次获取某个视频的访问量时,可以取到这 10 个域的值求和即可

    假设每分钟视频的访问量的上限是 255 ( 2^8,这里是为了控制溢出,值可以无限大小,只要是 2 的正整数倍即可)


    新增访问量时,只需计算应该往哪个 /哪些域( offset )里增加就可以了,如 22:13 的视频 123456 的访问量新增 17,根据时间计算 offset 为 3,命令:

    `BITFIELD v:123456:cnt:bit OVERFLOW SAT INCRBY u8 0 17

    这条命令返回对 offset 为 0 的域进行了+17 的操作,u8 表示按照 10 个 bit 分域,上限 255,0 标识 offset,即第 0 分钟,`OVERFLOW SAT`表示如果 incr 后的结果超过上限(这里是 2^8 ),那么结果保持在最大值 255 ( 8 位全 1 )



    获取视频 123456 近 10 分钟的访问量,命令:

    `BITFIELD v:123456:cnt:bit GET u8 GET u8 1 GET u8 2 GET u83 GET u8 4 GET u8 5 GET u8 6 GET u8 7 GET u8 8 GET u8 9`

    这条命令会返回每个域(即每分钟)的值,求和后即为近 10 分钟的累计访问量

    BITFIELD 的每个子命令的复杂度是 O(1)的,如果访问 /操作 N 个视频的近 10 分钟的访问量,也就是操作 N 次 Redis 即可



    方案的优点

    1. 省空间,bitmap 占用空间很小
    2. 支持批量,BITFIELD 的子命令可以多个同时操作

    缺点:

    1. 不是严格的滑动窗口,有一定的精度损失

    这个可以通过拆细粒度来解决,如 10 秒(甚至 1 秒)一个域,相应地,这样会增加一定的存储

    2. 设计时需要考虑单个单位时间内的上限,超过上限时,统计不准,因为我们在溢出控制时使用了饱和算法( SAT )

    这个可以在设计初期尽量预留一个保险的值,当然了,越大的话,存储也会越大

    3. 对于读写分离的场景(即读从写主),BITFIELD 被 Redis 标识为写命令,所以所有的 BITFIELD 都会在主节点上执行

    这个问题我们遇到了,但没有造成很高的负载,所以没有处理;不过阿里云有篇文章可以参考: https://developer.aliyun.com/article/757841
    waibunleung
        20
    waibunleung   232 天前
    @ch2 这种方案的话,还不如先写到 redis 一分钟同步一次数据到 mysql,基本没有写压力,方案上更趋近于这个
    waibunleung
        21
    waibunleung   232 天前
    @opengps 嗯嗯,这种可以考虑一下
    waibunleung
        22
    waibunleung   232 天前
    @vebuqi 看懂了大部分,但疑问是下一个 10 分钟的时间窗口,是另起新的 key 统计吗?还是将旧的清零?比如 10:59 分 过渡到 11:00 的时候,此时 bitmap 上 0 这个 field 还是存储着 10:50 的统计?还是怎么样
    angryfish
        23
    angryfish   232 天前 via iPhone
    你这小程序多大的量,还担心数据库写入太频繁。用户访问一次,写入一条记录,大多数情况下没什么问题
    keakon
        24
    keakon   232 天前
    总视频数、每 10 分钟总的访问量说下量级吧,应用场景是怎样的,感觉你想得太多了。

    比如「我想做一个管理后台…」,你管理后台每秒能刷几千次啊,还能被刷爆?

    而如果是面向公众用户的场景,你的需求也不需要「实时、精确地计算出访问增量」,每分钟统计一下就行了。

    最后,这种情况比较常见的做法是使用时间轮,比如选一个比 10 大又能被 60 整除的数,例如 12 。将当前的分钟数对 12 取模,然后每个 key 记录这一分钟的数据。每到一个新的一分钟,就把 11 分钟前的 key 删了。要获取数据时,就取当前和之前的 10 个 key 。
    至于数据结构选啥,根据你的视频量级、视频数是否恒定、id 是否连续等而定,不同的方案内存差别很大。比如一楼提到的 view_count_{vid}_{minute},这种实现不用看也知道内存扛不住。
    xuanbg
        25
    xuanbg   232 天前
    用 hash 类型存储,每个文件一个 key 。这个 key 下面每 10 分钟新增一个 hash key,在这个时间段里,每访问一次这个 hash key 的 value +1 。
    vebuqi
        26
    vebuqi   232 天前
    @waibunleung

    单个视频的访问量用两个 key 来存储,过期时间设置成两个周期,也就是 20 分钟
    写入时,每次都对当前周期内的 key 写入
    读取时,读取上个周期的后半部分和这个周期的前半部分

    如:
    现在是 10:56,则目前数据库中的 key 有两个,v:${id}:cnt:bit:1040 (过期时间还有 4 分钟) 和 v:${id}:cnt:bit:1050 (过期时间还有 14 分钟),1040 代表 10:4X 周期内的计数,10:50 代表 10:5X 周期内的计数

    若 10:56 新增 10 个,则对 v:${id}:cnt:bit:1050 内第 6 个域 incr 10 即可
    若读取最近 10min 的访问量,则取前一个 key 的后 3 个域( 10:47 、10:48 、10:49 )和当前 key 的前 7 个域( 10:50 ~ 10:56 ),然后求和

    这样的话就避免了你提到的问题,但增加了一倍的存储(两个 key )

    再复杂一点的方案,单 key 中前 N 位用 1 个单独的域,记录上次写入时间,每次写入前,根据当前时间和上次写入时间判断是否要重置之前的某个 /某些域,然后再进行写入+(重置)的操作。这样的话,存储能降到一个 key,但写入时多了一次操作,时间换空间了,适用于 写少读多的场景,所以最好是聚合写
    luzhh
        27
    luzhh   232 天前
    每次请求写到 log 里用 shell 统计都很快的。先实现出来,然后在看满足不满足性能要求和功能需求,然后在看怎么优化改进方案。
    waibunleung
        28
    waibunleung   232 天前
    @angryfish 日活目前在 1w 多有,预计会增长至 10w
    waibunleung
        29
    waibunleung   232 天前
    @keakon 就算用一楼的办法,每分钟也只是同一个普通的 key 。而且也会删掉,”不用看也知道内存扛不住“ 是不是太武断了些....我觉得抗住是没有问题的...
    waibunleung
        30
    waibunleung   232 天前
    @xuanbg 那这样子就没有了时间滑动窗口了,我也不会问这个问题了
    waibunleung
        31
    waibunleung   232 天前
    @luzhh 是的,今天想到了这个,直接解析 nginx 的 log 也是个办法
    luzhh
        32
    luzhh   232 天前
    或者将每一条视频播放请求发到队列用 flink 的滑动窗口去统计,你那服务器的配置干这个完全不在话下。
    xuanbg
        33
    xuanbg   232 天前
    @waibunleung 你要怎样滑动?以什么为单位?要以分钟为单位滑动,按分钟计数,按秒滑动就按秒计数。不还是一样的道理吗?
    siweipancc
        34
    siweipancc   232 天前 via iPhone
    @vebuqi 最近有个类似的需求,感谢
    billlee
        35
    billlee   231 天前
    说个 100K QPS 级的方案:

    1. 在内存(或 redis )里记数,定时 (比如 10s) flush 到持久存储 (mysql 或 influxdb) 并 reset 内存中的计数器
    2. 查询的时候就是指定时间范围 SUM() 聚合了
    3. 定期清理持久存储中的历史数据
    4. 如果需要保留长时间历史数据,需要在持久存储那端做降采样

    关键是 1. 写入的时候不要每次请求都落盘 2. 查询的时候要能用上 rdbms 的查询能力
    liuhan907
        36
    liuhan907   231 天前 via Android
    如果是单纯的统计增速,那用时序数据库不是很合适么?比如用开源版的 influxdb 。
    waibunleung
        37
    waibunleung   231 天前
    @xuanbg 换成一分钟之后,那这不就是一楼说的那种做法吗?
    xuanbg
        38
    xuanbg   231 天前
    @waibunleung 楼主你有没有搞清楚自己的需求?需求确定了,数据结构自然就能确定了。数据结构确定了,存储方案自然就能确定了。即使有问题,至于连问题是什么都说不清吗?
    waibunleung
        39
    waibunleung   231 天前
    @billlee 嗯嗯,有想过这个方法,历史数据可以放到冷数据表,但是这种方法如果要查更长的近 x 分钟时间的数据,视频越多,存储量就越多了,不知道你这个量级,一个固定时间内(比如 1 个小时),持久存储里面的记录数有多少?存储结构是怎么样的?
    比如 10s flush 一次到 db 的话,一分钟就相当于有 6 条记录了?
    waibunleung
        40
    waibunleung   231 天前
    @xuanbg 聊场外就没意思了,需求描述得挺清楚的了,就是要在内存有限的情况下用 redis 统计近 10 分钟的数据,做法不同数据结构不同,你后面说的这种做法和一楼二楼说出来的没有本质的区别,问题也在描述的地方说了,存在滑动窗口的问题,你的第一条回复本质是固定的时间窗口,我只是回复你这样做的话不满足我的需求,请问怎么看出我不了解需求和数据结构的问题呢?
    waibunleung
        41
    waibunleung   231 天前
    @liuhan907 嗯嗯我加入备忘研究一下
    keakon
        42
    keakon   231 天前
    @waibunleung 你最好先熟悉 redis 的内存占用再来臆测。一楼的方案内存占用要多 1~2 个数量级。
    waibunleung
        43
    waibunleung   230 天前
    @keakon 关键是我没有看出来你的方案和一楼的方案有什么本质的区别,臆测倒没有,你说扛不住也要说说你的理由和分析吧?
    waibunleung
        44
    waibunleung   230 天前
    @keakon 对不起,一开始没有 get 到你全部的意思,我之所以会这么说,是因为用一楼的方案,也可以做到你说的时间轮的类似效果,只是一楼少了定时删除旧缓存的做法,加上之后,我觉得和你说的没有太本质的区别,始终维护的是一个时间段内的数据
    sunhuawei
        45
    sunhuawei   230 天前
    看需求有点云里雾里的,如果是我的话我会用 HyperLogLog 统计,最大占用 12k,会有 0.81%左右的误差但很高效
    waibunleung
        46
    waibunleung   230 天前
    @sunhuawei 要在内存有限的情况下用 redis 统计近 10 分钟的视频访问量,转发数等数据,HyperLogLog 主要是去重,场景不对
    11373450
        47
    11373450   230 天前
    redis 设置一个过期时间( 10 分钟),然后设置他值为 1 。 判断这个 redis 存在就累加。没有存在就重新设置
    waibunleung
        48
    waibunleung   229 天前
    @11373450 这种不太符合时间滑动窗口的需求哈
    CantSee
        49
    CantSee   219 天前
    设置 bitmap 的存储大小,例如 1 -N 分钟,单个 Key 的长度为 200(假设),那么第一个 key 存储时,为 name_1,第二个为 name_2,同时失效第一个,保证永远只会存在一个 key,统计时,直接 200 * n +当前 name_key 的长度;
    关于   ·   帮助文档   ·   API   ·   FAQ   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   3864 人在线   最高记录 5497   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 08:21 · PVG 16:21 · LAX 00:21 · JFK 03:21
    ♥ Do have faith in what you're doing.