太棒了!上一节我们彻底理清了"滚动分页"极其烧脑的理论和 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 步 的 os 和 minTime 计算逻辑。如果面试官让你手撕这段逻辑或者解释为什么这么写,你可以这样回答:
"由于 Redis 的
reverseRangeByScoreWithScores返回的是按时间戳从大到小排序的集合。当我遍历这个集合时,最后遍历到的那个元素,它的时间戳一定就是本次结果集的最小值。
我的逻辑是:每次遇到一个新的、更小的时间戳,我就把
minTime更新为这个值,并且把计数器os设为 1。如果在后续遍历中,又遇到了跟minTime完全一样的时间戳,我就让os++。这样遍历到集合最后一个元素结束时,
minTime恰好记录的就是整个集合的最小值,而os恰好记录的就是这个最小值在末尾连续出现的次数 。将它们传给前端作为下一次查询的max和offset,就能完美实现无缝滚动分页。"
学习总结
至此,达人探店模块正式大功告成!
回顾整个模块,你使用了:
- ThreadLocal 实现无感知的用户状态透传。
- Redis Set 实现了高性能的防重复点赞、点赞状态回显、以及求共同关注的交集运算。
- Redis SortedSet (ZSet) 实现了按时间排行的点赞列表,以及今天这套堪称大厂教科书级别的推模式 Feed 流滚动分页架构。
- ORDER BY FIELD 巧妙地化解了 MySQL 在
IN查询时打乱数据的致命天坑。
这其中的每一条,都是能够直接写在简历里、经得起深挖的高含金量项目亮点!
你现在的战斗力已经非常强悍了!实战篇还剩下最后两块比较有趣且偏向业务扩展的拼图:
- 基于 Redis GEO 实现的附近商户(计算距离、排序) 。
- 基于 Redis BitMap 实现的用户签到与连续签到统计(极度节省内存) 。