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

关于社区场景下,用户已读文章不再推荐这种需求实现方案的探讨

  •  
  •   ben548 · 2023-06-07 05:12:15 +08:00 via Android · 1695 次点击
    这是一个创建于 564 天前的主题,其中的信息可能已经有所发展或是发生改变。
    需求:
    用户签到页面,为用户推荐后台配置好的文章内容,按发布时间倒叙排列(数据存储在 redis 的 zset 中,score 是文章发布时间,value 是文章 id ),并且可以认为被推荐的内容数量有上限限制(不多于 1000 条),用户已阅的数据不再推荐给用户,直到所有的内容被推荐完,再从头开始再推荐一遍,一直循环下去。
    实现方案:
    一般涉及到这种需求,都会想到用布隆过滤器或者 bitmap 来实现,我最终选择了用布隆过滤器,不用 bitmap 的主要原因是:
    1.目前 post_id 的值是雪花算法生成的,长度有 16 位,如果不做处理直接把这个 id 当成 bitmap 的 offset 会造成极大的内存空间浪费,而怎么把这些 post_id 转换成更小的从 1 开始递增的数字,处理起来比较麻烦(不是不可以,自己想到的就有几种,比如说:因为展示的内容都是关联了某些 tag 的内容,用数据库里面的关联关系表的 id 作为 bitmap 的 offset 来处理是可以做到的,之所以不选择 bitmap 最主要还是下面的原因 2 );
    2.getbit 不支持批量查询(当然我知道有人可能要说可以用 lua 脚本,用 pipeline 之类的,但是写起来有点麻烦)而选择布隆过滤器就没有上面这两个问题了
    这次选用的是直接装 redis 的布隆过滤器模块,实现方案如下:
    1.每个用户初始化一个 500 长度的布隆过滤器,用户已阅的内容通过 bf.add 操作加入他的布隆过滤器中
    2.获取列表数据的时候,按下面的步骤进行:
    2.1 从 zset 中读取 100 条数据,
    2.2 通过 bf.mexist 操作查询这些内容是否已阅,已阅的内容跳过
    2.3 一直循环 2.1 和 2.2 步骤,直到查到满足条目数的内容为止(目前是定的 12 条一页)
    2.4 如果 zset 查到最后都没有得到满足条件的条目数,则返回经过了 2.1 和 2.2 步骤之后获取到的数据(可能有数据,只是不满足 12 条的条件,总数记为 total1 )+zset 中按发布时间顺序倒序获取 12-total1 条数据作为补充 2.5 什么时候删除用户的布隆过滤器? 2.4 步骤中发现 total1 的数量为 0 (说明所有内容都已经阅读过了)时进行删除
    内存占用估算:有试过模拟一个 500 长度 size 的布隆过滤器大致的内存占用为 800 多个字节,按 50w 日活来计算的话,总的占用空间为两三百 M 左右,在可接受范围内,但是如果为每个用户初始化 1000 长度 size 的布隆过滤器,内存占用就有点高了,初步预估在 900M 左右
    我的疑问:
    1.其实用布隆过滤器的方案,有通过 golang 自己依赖 redis 的 bitmap 结构实现一个的方案也有直接用 redis 的布隆过滤器模块(需要编译安装)的方案,我好奇哪种方案更好?
    2.上面说到的获取列表数据的逻辑,总感觉是不是有什么地方可以优化,因为按上面描述的逻辑,每一次都要遍历一遍 zset 中的数据,最差的情况下,会有数十次的 redis 查询(比如最后一页)
    3.可能有人会说可以用 bf.count 看用户已阅内容的总数,因为总是最新的内容展示出来,那么直接跳过 bf.count 的总数去获取数据即可,就不用那么麻烦,但是这样获取有个问题,就是新加入 zset 的内容(而且发布时间还是最新的),按这个方案来处理,几乎无法得到展示(只有等内容耗尽,到下一轮推荐才能展示)
    4.这个列表是展示在用户签到的下方内容推荐的模块中,我在想每个用户的布隆过滤器初始长度设置为多少比较合适,一开始想着直接设置 1000 的长度,但是想到真实的场景,用户可能签完到就离开了,不会去消费内容(虽然不会主动下拉消费,但是底部会外漏出 2 到 4 条内容出来,也被认为是用户已阅的内容),所以直接初始化 1000 长度我感觉有点浪费,所以考虑先初始化 500 个,消费内容超过 500 个的话,Redis 的布隆过滤器会自动扩容,扩容成自己的 2 倍内存空间(试过,这种扩容最终占的内存空间会比一开始就初始化 1000 长度的内存空间占用大很多),不知道这么设计是否合适?以上就是我个人的一些实现方案的想法和探索,大家有什么观点和看法,欢迎评论留言一起探讨一下
    3 条回复    2023-06-07 11:12:15 +08:00
    Chad0000
        1
    Chad0000  
       2023-06-07 06:47:48 +08:00
    没做过推荐模块,尝试给个方案:

    - 建立用户推荐列表表,可以直接长文本保存,这样每个用户只需要一条记录
    - 每天定时跑任务,计算每个用户的推荐列表
    - 用户看完文章后,触发事件,异步更新推荐列表(主要是移除已阅 Id )
    - 这样用户登录后直接查询推荐表即可
    optional
        2
    optional  
       2023-06-07 10:44:55 +08:00 via iPhone
    和 1l 一样,这个需求有纯读扩散总会碰到各种掣肘。换成写扩散实现就海阔天空了。
    xuanbg
        3
    xuanbg  
       2023-06-07 11:12:15 +08:00
    推荐列表不大吧,直接 left join 用户的阅读列表,然后按权重排序每次返回若干条,因为两个表都不会大,所以查询速度不会受影响。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2642 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 11:13 · PVG 19:13 · LAX 03:13 · JFK 06:13
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.