关注推送-Feed流

前言(技术选型):

Feed流

  1. 概念:Feed 流(信息流),即"关注页"看到的动态列表。类似于微信朋友圈、微博关注页,内容由用户关注的人发布,按时间顺序排列。
  2. 需求:在项目中,当 A 用户发布了一篇探店笔记(Blog),关注了 A 的粉丝应该能在自己的"关注"列表里刷到这篇笔记。

推模式

  1. 原理:博主发笔记时,主动推送到所有粉丝的收件箱(Redis)。
  2. 优点:读操作极快,直接读自己的收件箱即可。
  3. 缺点:大V发贴由于粉丝多,写入压力大(适合中小规模用户体量)。

Redis ZSet

  1. 设计:我们将 BlogId 作为 Value,时间戳作为 Score。
  2. 策略:不按"第几页"查,而是按"在这个时间之前"查。
    第一次查:查当前时间之前的 10 条。
    第二次查:查上一页最后一条动态的时间戳之前的 10 条。
  3. 优势:无论头部插入了多少新数据,只要我记得上次读到了哪个时间点,我就能准确地接着往下读,完全不受新数据插入的影响

推送笔记

流程

  1. 保存笔记到数据库。
  2. 查询当前用户的所有粉丝 。
  3. 循环推送:遍历粉丝,通过 stringRedisTemplate.opsForZSet().add(key, blogId, timestamp) 将笔记 ID 写入每个粉丝的 Redis ZSet 中。
    代码实现
scss 复制代码
@Override  
public Result saveBlog(Blog blog) {  
    // 获取登录用户  
    UserDTO user = UserHolder.getUser();  
    blog.setUserId(user.getId());  
    // 保存探店博文  
    blogService.save(blog);  
    //查询作者所有粉丝  
    List<Follow> fans = followService.query().eq("follow_user_id", user.getId()).list();  
    if (fans == null || fans.isEmpty()){  
            return Result.ok();  
    }  
    //推送笔记id给所有粉丝  
    for (Follow fan : fans) {  
        String key = FEED_KEY + fan.getUserId();  
        stringRedisTemplate  
                .opsForZSet()  
                .add(key, blog.getId().toString(), System.currentTimeMillis());  
    }  
    //返回id  
    return Result.ok(blog.getId());  
}

注意事项

将笔记推送到粉丝邮箱时,以当前时间戳作为分数。

读取推送

流程

  1. 数据处理:lastId为上次查询数据的最大时间戳,offset为偏移量,如果是第一次查询,两者可能为空,因此要防止出现空指针异常。
  2. 查询redis邮箱:
  • key为要查询的key
  • 0, maxScore为分数查询范围
  • safeOffset为偏移量
  • 2为每页展示数据
  1. 处理查询结果,得到本次查询的lastId和safeOffset。
  2. 查询数据库返回数据。

代码实现

ini 复制代码
@Override  
public Result queryBlogOfFollow(Long lastId, Integer offset) {  
    // 获取当前用户  
    Long userId = UserHolder.getUser().getId();  
    // 防御式处理,避免请求参数缺失导致空指针  
    long maxScore = (lastId == null || lastId <= 0) ? Long.MAX_VALUE : lastId;  
    int safeOffset = (offset == null || offset < 0) ? 0 : offset;  
  
    // 查询收件箱 ZREVRANGEBYSCORE key Max Min WITHSCORES LIMIT offset count    String key = FEED_KEY + userId;  
    Set<String> ids = stringRedisTemplate.opsForZSet()  
            .reverseRangeByScore(key, 0, maxScore, safeOffset, 2);  
    if (ids == null || ids.isEmpty()) {  
        return Result.ok();  
    }  
  
    // 解析出blogId和score  
    List<Long> blogIds = ids.stream().map(Long::valueOf).collect(Collectors.toList());  
    long minTime = 0;  
    int os = 1;  
    for (String id : ids) {  
        Double score = stringRedisTemplate.opsForZSet().score(key, id);  
        if (score == null) {  
            continue;  
        }  
        long time = score.longValue();  
        if (time == minTime) {  
            os++;  
        } else {  
            minTime = time;  
            os = 1;  
        }  
    }
      
    // 根据id查询blog  
    String idStr = StrUtil.join(",", blogIds);  
    List<Blog> blogs = query().in("id", blogIds)  
            .last("ORDER BY FIELD(id," + idStr + ")")  
            .list();  
    for (Blog blog : blogs) {  
        queryBlogUser(blog);  
        isBlogLiked(blog);  
    }  
    ScrollPageResult scrollPageResult = new ScrollPageResult(blogs, minTime, os);  
    return Result.ok(scrollPageResult);  
}

注意事项

  1. redis查询命令为reverseRangeByScore,按分数(时间戳)倒序查找范围内的元素。
  2. lastId和safeOffset的获取:增强for按顺序遍历ids集合,拿到的score是递减的,如果当前时间time与已经记录的minTime不等,那么time一定是更小的,将time赋值给minTime并将偏移量重置为1。如果当前时间time与已经记录的minTime相等,偏移量自增1。
  3. 数据库查询:如果直接使用in操作符则查询结果是无序的,使用last()方法手动添加ORDER BY FIELD,确保数据库查询结果有序
相关推荐
敲代码的嘎仔3 小时前
用 Redis 合并写 + DelayQueue 延迟检测,把视频播放进度写库频率降到 1/60
java·开发语言·数据库·redis·缓存·音视频·高并发
程序猿乐锅5 小时前
【黑马点评 | 第一篇】从 Session 到 Redis+Token 的改造与拦截器实现
数据库·redis·缓存
野生技术架构师5 小时前
Redis Hot Key 与 Big Key:危害、发现与解决方案
数据库·redis·缓存
学渣超6 小时前
从一次早高峰数据库告警说起:你真的理解缓存该如何落地应用吗?
redis·后端·架构
程序员清风7 小时前
Python 操作 MySQL、Redis 与消息队列的完整实践
redis·python·mysql
momo0611721 小时前
Redis新手入门 -- 学习笔记
redis·后端
吃饱了得干活1 天前
Redisson 进阶实战:看门狗机制与分布式锁完全指南
java·redis·后端
我是苏苏1 天前
Redis开发09:Windows系统下主从复制的配置教程
数据库·redis·缓存
写后端的胖头鱼1 天前
【高频面试题】如何找出redis中的慢查询记录
redis·慢日志
彧azz2 天前
Redis 全面指南:从核心概念到高可用架构实战
数据库·redis·架构