日活千万,同一浏览器同一天内,要求不同语言推荐文章时不重复曝光

**方案一(Redis 全局池子 + 每用户已读集合)**

总述:每天由定时任务从数据库按游标分页取出一批文章,写入 Redis 全局候选池,所有用户共享同一池子;每个用户按 `dedupToken` 在 Redis 里维护当天已曝光集合,推荐时用 Lua 脚本从池中采样、`SADD` 原子去重后返回,候选不足则少返回或不返回,严格保证同一天不重复推荐。

1. **候选池**:每天零点由单任务通过数据库分页游标逐页取出固定数量文章(池子上限锁死在 100~150,绝不超过线上 `set-max-intset-entries`),先写临时 key 再 `RENAME` 为 `recommend:pool:<ymd>:<cursor>`,全局所有实例读同一份。
2. **用户已读集合**:key 为 `user:read:<dedupToken>:<ymd>`,`dedupToken` 由服务端下发 HttpOnly Cookie,文章 ID 为 8~10 位整数,池子 ≤512 时保持 intset 编码,不会转 hashtable。
3. **原子去重**:推荐全程用 Lua 脚本完成“采样 → 逐个 `SADD` → 按返回值(1=可推、0=已读)取舍 → 返回结果”,选中即标记,避免高并发下查写分离导致重复推荐。
4. **过期策略**:key 创建时即用 `EXPIREAT` 设到次日缓冲时刻并加随机 1~2 小时抖动,实现自然日语义、错峰过期。
5. **降级策略**:采样若干篇后凑不够所需篇数时少返回或返回空,绝不回退到已曝光文章,符合“不重复优先于多推”的核心取舍。
6. **容量约束**:千万日活 + 20GB 内存下,池子 100 时单 key 约 1KB、总量约 10GB(含碎片 12~13GB)安全;池子 150 约 14GB 已冒险;池子 500 约 50GB 不可行。若 Redis 为 Cluster 模式,Lua 只操作 user key,候选由 PHP 侧从 pool 取出后作为参数传入,避免跨槽。

**方案二(仅用 Cookie 记录游标)**

总述:不依赖任何服务端去重状态,浏览器 Cookie 里只存一个数据库查询游标,每次请求按“语言 + 游标”从数据库按时间降序取 2 篇,返回后把新游标写回 Cookie,同一天内游标单调前进,天然不重复,适合测试阶段快速验证。

1. **游标存储**:Cookie 里存游标值(可附带日期字段用于跨天重置),语言由域名/路径/配置固定确定,用户无切换语言入口,因此一个游标值即可,无需 JSON 字典。
2. **查询逻辑**:每次请求执行 `WHERE language=? AND id < cursor ORDER BY id DESC LIMIT 2`,取到 2 篇后把最后一条的 ID 写回 Cookie 作为新游标。
3. **去重原理**:游标只前进不后退,同一天内取到的文章段连续且不重叠,不需要记录“看过哪些文章 ID”,服务端零状态。
4. **兜底逻辑**:查不到 2 篇(文章被删或游标失效)时回退到从最新开始取并重置游标;Cookie 里带日期,跨天时重置游标,保证每天从最新开始。
5. **已知限制**:Cookie 会被清、会被篡改,隐私模式下关窗即重置,正常模式多窗口共享但并发同时请求仍可能重复;推荐顺序固定为时间降序,无法做“随机池子不重复”,仅适用于测试需求。


欢迎大家评论留言

posted @ 2026-11-04 13:06  gentsir  阅读(6)  评论(0)    收藏  举报