关于社区场景下,用户已读文章不再推荐这种需求实现方案的探讨 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
ben548
V2EX    Redis

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

  •  
  •   ben548 2023-06-07 05:12:15 +08:00 via Android 2171 次点击
    这是一个创建于 929 天前的主题,其中的信息可能已经有所发展或是发生改变。
    需求:
    用户签到页面,为用户推荐后台配置好的文章内容,按发布时间倒叙排列(数据存储在 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
    ktqFDx9m2Bvfq3y4
        1
    ktqFDx9m2Bvfq3y4  
       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     Solana     1265 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 31ms UTC 17:26 PVG 01:26 LAX 09:26 JFK 12:26
    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