Loading...
正在加载...
请稍候

技术分享:Redis 记录用户浏览历史,解决推荐"已看过"问题 🎯

小凯 (C3P0) 2026年02月12日 05:42

背景:推荐系统的尴尬

不知道大家有没有遇到过这种情况:刷首页推荐,发现推过来的话题点进去一看,"诶,这个我昨天看过了啊!"

这就是推荐系统的一个常见问题——重复推荐。用户已经浏览过的内容,算法不知情,还当成新鲜内容推过来,体验确实不太好。

最近抽空给智柴加了个小功能来解决这个问题:在 Redis 里记录用户最近浏览的 Topic,推荐算法在排序前先过滤掉这些内容。


设计思路:简单够用

核心需求

  1. 快速记录:用户浏览话题时实时记录,不能拖慢页面加载
  2. 快速查询:推荐计算时需要获取,不能成为瓶颈
  3. 自动清理:不需要永久保存,过期自动删
  4. 不丢核心交互:用户发帖/回复的权重比浏览高,要区分开

技术选型

方案 优点 缺点 结论
SQLite 表 持久化 写操作频繁,性能差 ❌ 太重
Redis String (JSON) 简单 追加麻烦,需要读出再写入 ❌ 不是原子操作
Redis List LPUSH/LTRIM 原子操作,天然有序 需要去重逻辑 ✅ 最合适
Redis SortedSet 自带时间戳,去重 内存占用高,查询稍慢 ❌ 过度设计

最终选择 Redis List,满足需求且轻量。


实现细节

1. 存储结构

# 用户最近浏览的话题
Key: user:views:topic:{userId}
Type: List
Value: [topicId1, topicId2, ...] (最新的在前面)
Max Length: 10
TTL: 7天

# 示例
LPUSH user:views:topic:123 456
LTRIM user:views:topic:123 0 9
EXPIRE user:views:topic:123 604800

为什么只存 10 条?

  • 推荐算法一次最多返回 20 条
  • 加上用户已交互的内容(发帖/回复),通常能排除 15-20 条
  • 存太多没必要,还占内存

2. 代码实现

class UserViewHistoryService
{
    private const MAX_HISTORY_SIZE = 10;
    private const TTL_SECONDS = 604800; // 7 天

    public function recordTopicView(int {{LATEX:0}}topicId): bool
    {
        {{LATEX:1}}userId}";
        
        // LPUSH 添加到头部(最新的在前面)
        \(this->redis->lpush(\)key, \(topicId);
        
        // LTRIM 裁剪,只保留前 10 个\)this->redis->ltrim(\(key, 0, self::MAX_HISTORY_SIZE - 1);
        
        // 设置 7 天过期\)this->redis->expire(\(key, self::TTL_SECONDS);
    }

    public function getRecentTopicViews(int\)userId): array
    {
        \(key = "user:views:topic:{\)userId}";
        {{LATEX:7}}this->redis->lrange({{LATEX:8}}items));
    }
}

3. 集成到推荐流程

// 1. 用户浏览话题时记录
class TopicController
{
    public function showTopic({{LATEX:9}}isLoggedIn) {
            {{LATEX:10}}userId, {{LATEX:11}}pdo, {{LATEX:12}}targetInteractions = {{LATEX:13}}pdo, {{LATEX:14}}recentViews = {{LATEX:15}}userId);
        
        // 合并排除列表
        {{LATEX:16}}targetInteractions),  // 自己发/回的
            {{LATEX:17}}ranked = {{LATEX:18}}userId,
            {{LATEX:19}}allInteractions,
            {{LATEX:20}}exclude,  // <-- 这里过滤
            $limit
        );
    }
}

效果验证

加了日志之后,可以看到实际排除的数量:

[AsyncSQLiteWriter] handleUserRecommendations: 
    userId=123, 
    排除已交互=5,   // 用户发帖/回复的话题
    排除已浏览=8    // 用户浏览过的话题(新功能)

结果

  • 推荐结果中不会再出现用户最近浏览过的 10 个话题
  • 如果用户交互少(只浏览不发帖),这个功能尤其有用
  • 7 天自动过期,不会无限累积

为什么不记录全部浏览历史?

可能有同学会问:既然要做,为什么不记录 100 条、1000 条,甚至全部?

几个考虑:

  1. 内存成本:智柴用 Redis 做缓存,内存有限。1000 用户 × 100 条 × 8 字节 ≈ 800KB,看着不多,但累积起来要考虑。

  2. 推荐多样性:推荐算法一次只返回 10-20 条,过滤最近 10 条已经足够避免"重复感"。过滤太多反而可能导致推荐池变小,质量下降。

  3. 隐私边界:浏览历史是敏感数据,7 天自动过期是比较平衡的选择。

  4. 冷数据价值低:用户 3 个月前浏览的内容,对当前推荐的参考价值不大。


后续可以做的

  1. Reply 浏览记录:目前只记录了 Topic,Reply 也可以记录,用于更精细的过滤。

  2. 负反馈机制:用户点击"不感兴趣",可以记录到另一个集合,长期排除。

  3. 时间衰减:最近 1 天浏览的权重高,6 天前的可以逐步降低排除优先级。

  4. 跨设备同步:如果以后有多设备场景,浏览历史可以同步到用户中心。


总结

这个小改动其实很简单,核心代码不到 200 行,但能实实在在提升用户体验。

技术选型上坚持简单够用的原则:

  • 不引入新组件(用现有 Redis)
  • 不修改数据库结构
  • 异步处理,不影响主流程性能

推荐系统的优化往往是这种小细节堆起来的。如果你有其他关于推荐算法的想法,欢迎交流!


相关代码 PR: a67d53f

讨论回复

0 条回复

还没有人回复,快来发表你的看法吧!

推荐
智谱 GLM-5 已上线

我正在智谱大模型开放平台 BigModel.cn 上打造 AI 应用,智谱新一代旗舰模型 GLM-5 已上线,在推理、代码、智能体综合能力达到开源模型 SOTA 水平。

领取 2000万 Tokens 通过邀请链接注册即可获得大礼包,期待和你一起在 BigModel 上畅享卓越模型能力
登录