太棒了!在上一节中,我们经过深思熟虑,最终为黑马点评项目选择了**“推模式(Push)”**作为 Feed 流的底层架构。并且明确了要使用 Redis 的 SortedSet (ZSet) 作为每个粉丝的“收件箱”。
现在,是时候把理论转化为代码了!我们需要去改造最开始写的**“发布探店笔记”**的接口。当博主点击发布的那一瞬间,系统不仅要把笔记存入数据库,还要像发传单一样,把笔记推送给所有的粉丝。
📚 实战篇 08. 好友关注 - 推送到粉丝收件箱学习文档
一、 业务逻辑拆解
核心动作:改造现有的 saveBlog (发布笔记) 方法。
在最开始写 saveBlog 时,我们的逻辑仅仅是:获取当前用户 ID -> 封装 Blog 对象 -> 存入 MySQL 数据库。
现在引入了 Feed 流,业务流程必须向后延伸:
-
保存笔记到 MySQL(原逻辑保持不变,确保数据持久化)。
-
查询粉丝列表: 去
tb_follow关注表中,查出所有关注了当前博主(当前登录用户)的粉丝 ID。 -
循环推送(写扩散): 遍历这些粉丝 ID,将刚刚保存成功的笔记 ID,逐个存入每个粉丝专属的 Redis
ZSet收件箱中。- Key 设计:
feed:{fanId}(例如:feed:1001代表用户 1001 的收件箱)。 - Value: 笔记的
blogId。 - Score: 当前的时间戳(利用时间戳的天然递增特性,让 ZSet 自动按照时间先后顺序排好序)。
- Key 设计:
二、 核心代码落地
我们需要来到 BlogServiceImpl 中,修改 saveBlog 方法的代码:
Java
@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {
@Resource
private IFollowService followService; // 注入关注服务,用于查粉丝
@Override
public Result saveBlog(Blog blog) {
// 1. 获取当前登录用户 (博主)
UserDTO user = UserHolder.getUser();
blog.setUserId(user.getId());
// 2. 将探店笔记保存到 MySQL 数据库
boolean isSuccess = save(blog);
if (!isSuccess) {
return Result.fail("新增笔记失败!");
}
// ----------------- 以下为 Feed 流核心推送逻辑 -----------------
// 3. 查询当前博主的所有粉丝
// SQL: SELECT user_id FROM tb_follow WHERE follow_user_id = ?
List<Follow> follows = followService.query().eq("follow_user_id", user.getId()).list();
// 如果没有粉丝,直接结束并返回笔记 ID
if (follows == null || follows.isEmpty()) {
return Result.ok(blog.getId());
}
// 4. 循环遍历粉丝列表,将笔记 ID 推送到每个粉丝的 Redis 收件箱
for (Follow follow : follows) {
// 4.1 获取粉丝的 ID
Long fanId = follow.getUserId();
// 4.2 拼接粉丝专属的收件箱 Key
String key = "feed:" + fanId;
// 4.3 推送数据到 ZSet 中 (Value = 笔记ID, Score = 当前时间戳)
stringRedisTemplate.opsForZSet().add(key, blog.getId().toString(), System.currentTimeMillis());
}
// 5. 返回新增成功的笔记 ID
return Result.ok(blog.getId());
}
}
三、 架构进阶反思 (面试高频加分项)
这段代码虽然简单明了地实现了推模式,但在真实的千万级日活架构中,直接在主线程里这么写是存在隐患的。面试官非常喜欢在这里挖坑。
💥 隐患拷问:
如果在 for 循环推送 Redis 时,这个博主有 10 万个粉丝怎么办?
循环写 10 万次 Redis 会耗费大量的时间(网络 I/O 阻塞),导致前端用户点击“发布”后,页面卡住几秒甚至十几秒转圈圈,体验极差。
✅ 满分优化方案(你可以不写代码,但一定要知道思路):
- 异步解耦: 引入我们在秒杀优化中用到过的消息队列 (MQ) 或者 Spring 的
@Async异步线程池。 - 逻辑剥离: 主线程只要执行完第 2 步(存入 MySQL),就立刻给前端返回“发布成功”。
- 后台搬砖: 把“查粉丝 -> 循环推 Redis”的脏活累活,扔给后台异步线程去慢慢做。反正粉丝晚几秒钟看到新动态,是完全可以接受的(这叫“最终一致性”)。
学习总结与终极预警
这一节,我们彻底打通了**“博主发件 -> 粉丝收件箱”**的数据上行链路。所有的动态现在已经安安静静地躺在粉丝的 Redis ZSet 里,并且按时间排好序了。
⚠️ 终极大挑战预警:
数据推进去了,怎么把它读出来?
你可能会说:“用 ZRANGE 或者 ZREVRANGE 查出来不就行了吗?”
如果这是一个静态不变的列表,确实可以。但是 Feed 流是时刻在动态新增的!
如果你用传统的 LIMIT offset, size (通过下标) 来进行分页查询,在用户不断往下滑动页面的过程中,如果有新的动态插入到了列表头部,整个列表的下标都会向后错位,导致用户刷到重复的帖子!
为了解决动态列表的重复读取问题,我们必须引入一种极其硬核的分页思想——滚动分页 (Scroll Pagination) 。