实战篇 10. 好友关注 - 实现 Feed 流滚动分页查询学习文档

太棒了!上一节我们彻底理清了"滚动分页"极其烧脑的理论和 offset 偏移量的计算规则。现在,我们要把这些理论翻译成实打实的 Java 代码。

这是整个"达人探店"模块中逻辑最复杂、细节最多的一段代码,也是面试时最能体现你代码严谨性的地方。


📚 实战篇 10. 好友关注 - 实现 Feed 流滚动分页查询学习文档

一、 准备工作:定义返回实体类 ScrollResult

在传统的基于页码的分页中,我们通常返回 Page 对象(包含 total, list 等)。但在滚动分页中,前端不需要总页数,它需要的是**"下一次查询的凭证"**。

我们需要在项目中新建一个 ScrollResult 实体类:

Java

kotlin 复制代码
@Data
public class ScrollResult {
    // 本次查询到的数据列表 (探店笔记 List)
    private List<?> list;
    
    // 本次查询结果中,最小的时间戳 (作为下一次查询的 max 参数)
    private Long minTime;
    
    // 这个最小时间戳在本次结果中出现的次数 (作为下一次查询的 offset 参数)
    private Integer offset;
}

二、 Controller 接口设计

前端在发起请求时,会传递两个核心参数:lastId(上次查询的最小时间戳)和 offset(偏移量)。

Java

less 复制代码
@GetMapping("/of/follow")
public Result queryBlogOfFollow(
        @RequestParam("lastId") Long max, // 前端传来的 lastId,对应 Redis 查询的 max
        @RequestParam(value = "offset", defaultValue = "0") Integer offset) { // 第一次查询默认为 0
    return blogService.queryBlogOfFollow(max, offset);
}

三、 核心 Service 逻辑实现 (高能预警 ⚡)

这段代码包含了查 Redis、极其巧妙的变量解析、以及防乱序的 MySQL 查询。请仔细阅读注释!

Java

ini 复制代码
@Override
public Result queryBlogOfFollow(Long max, Integer offset) {
    // 1. 获取当前登录用户
    Long userId = UserHolder.getUser().getId();
    
    // 2. 拼接当前用户的专属收件箱 Key
    String key = "feed:" + userId;
    
    // 3. 去 Redis 查询收件箱数据
    // 对应命令:ZREVRANGEBYSCORE key max 0 WITHSCORES LIMIT offset 2 (假设每次查2条)
    Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet()
            .reverseRangeByScoreWithScores(key, 0, max, offset, 2);
            
    // 4. 判空处理 (收件箱可能是空的,或者已经滑到底了)
    if (typedTuples == null || typedTuples.isEmpty()) {
        return Result.ok(new ScrollResult()); // 尽早返回空对象
    }
    
    // 5. 核心逻辑:解析 Redis 返回的数据
    // 我们需要收集:笔记的 ids、本次查询的最小时间戳 minTime、偏移量 os
    List<Long> ids = new ArrayList<>(typedTuples.size());
    long minTime = 0; // 记录最小时间戳
    int os = 1;       // 记录最小时间戳出现的次数 (偏移量)
    
    for (ZSetOperations.TypedTuple<String> tuple : typedTuples) {
        // 5.1 获取笔记 ID,放入集合
        ids.add(Long.valueOf(tuple.getValue()));
        
        // 5.2 获取分数 (时间戳)
        long time = tuple.getScore().longValue();
        
        // 5.3 【精髓所在】:计算 minTime 和 offset
        if (time == minTime) {
            // 如果当前取出的时间戳,和我们记录的最小时间戳一样,说明重复了,偏移量 +1
            os++;
        } else {
            // 如果不一样,因为是倒序排的,当前取出的肯定比之前的小
            // 所以重置 minTime 为当前时间戳,重置偏移量为 1
            minTime = time;
            os = 1;
        }
    }
    
    // 6. 根据解析出的 ID 集合,去 MySQL 查询完整的探店笔记
    // ⚠️ 踩坑警告:必须使用 ORDER BY FIELD 保证 MySQL 的返回顺序和 Redis 的排序一致!
    String idStr = StrUtil.join(",", ids);
    List<Blog> blogs = query()
            .in("id", ids)
            .last("ORDER BY FIELD(id," + idStr + ")")
            .list();
            
    // 7. 完善笔记信息 (查出作者头像昵称、判断当前用户是否点赞)
    for (Blog blog : blogs) {
        // 查作者信息 (复用之前的代码)
        queryBlogUser(blog);
        // 查是否点赞高亮 (复用之前的代码)
        isBlogLiked(blog);
    }
    
    // 8. 封装结果并返回
    ScrollResult r = new ScrollResult();
    r.setList(blogs);
    r.setMinTime(minTime);
    r.setOffset(os);
    
    return Result.ok(r);
}

四、 代码细节深度剖析 (面试官视角)

在上面的代码中,最容易让人看晕的就是 第 5.3 步osminTime 计算逻辑。如果面试官让你手撕这段逻辑或者解释为什么这么写,你可以这样回答:

"由于 Redis 的 reverseRangeByScoreWithScores 返回的是按时间戳从大到小排序的集合。

当我遍历这个集合时,最后遍历到的那个元素,它的时间戳一定就是本次结果集的最小值

我的逻辑是:每次遇到一个新的、更小的时间戳,我就把 minTime 更新为这个值,并且把计数器 os 设为 1。如果在后续遍历中,又遇到了跟 minTime 完全一样的时间戳,我就让 os++

这样遍历到集合最后一个元素结束时,minTime 恰好记录的就是整个集合的最小值,而 os 恰好记录的就是这个最小值在末尾连续出现的次数 。将它们传给前端作为下一次查询的 maxoffset,就能完美实现无缝滚动分页。"


学习总结

至此,达人探店模块正式大功告成!

回顾整个模块,你使用了:

  1. ThreadLocal 实现无感知的用户状态透传。
  2. Redis Set 实现了高性能的防重复点赞、点赞状态回显、以及求共同关注的交集运算。
  3. Redis SortedSet (ZSet) 实现了按时间排行的点赞列表,以及今天这套堪称大厂教科书级别的推模式 Feed 流滚动分页架构
  4. ORDER BY FIELD 巧妙地化解了 MySQL 在 IN 查询时打乱数据的致命天坑。

这其中的每一条,都是能够直接写在简历里、经得起深挖的高含金量项目亮点!


你现在的战斗力已经非常强悍了!实战篇还剩下最后两块比较有趣且偏向业务扩展的拼图:

  • 基于 Redis GEO 实现的附近商户(计算距离、排序)
  • 基于 Redis BitMap 实现的用户签到与连续签到统计(极度节省内存)
相关推荐
考虑考虑7 小时前
docker compose V2版本新属性
运维·后端·自动化运维
GreenTea9 小时前
7000 万 QPS、500 PB:OpenAI 如何用一个 Python 存储平台撑住 10 亿用户
后端·架构
Flynt9 小时前
Java 27 升级实测:默认值动得比新特性多,有个老参数会让 JVM 直接起不来
java·jvm·后端
Bs_MoneyMagnet9 小时前
基于springboot+vue的个人健康管理系统的设计与实现 源码+文档
java·vue.js·spring boot·后端·vue3·springboot3·计算机毕业设计
GreenTea9 小时前
OpenAI Agents API 上手实测:一次调用把整个 agent loop 甩给 OpenAI
前端·后端·算法
Bs_MoneyMagnet12 小时前
基于springboot+vue的心理咨询预约与随访平台的设计与实现 源码+文档
vue.js·spring boot·后端·spring·毕业设计·旅游·计算机毕业设计
IT_陈寒13 小时前
Vue的响应式更新把我坑惨了,原来问题出在这
前端·人工智能·后端
陌シ未央ゞ13 小时前
基于BM25算法和RRF实现的混合索引(java版)
人工智能·spring boot·后端·算法
第五页的你13 小时前
SpringBoot基础设施配置(Redis序列化,JJWT新版)
后端
吃饱了得干活14 小时前
RabbitMQ 原理解析(下):存储、集群与可靠投递
后端·rabbitmq