实战篇 08. 好友关注 - 推送到粉丝收件箱学习文档

3 阅读4分钟

太棒了!在上一节中,我们经过深思熟虑,最终为黑马点评项目选择了**“推模式(Push)”**作为 Feed 流的底层架构。并且明确了要使用 Redis 的 SortedSet (ZSet) 作为每个粉丝的“收件箱”。

现在,是时候把理论转化为代码了!我们需要去改造最开始写的**“发布探店笔记”**的接口。当博主点击发布的那一瞬间,系统不仅要把笔记存入数据库,还要像发传单一样,把笔记推送给所有的粉丝。


📚 实战篇 08. 好友关注 - 推送到粉丝收件箱学习文档

一、 业务逻辑拆解

核心动作:改造现有的 saveBlog (发布笔记) 方法。

在最开始写 saveBlog 时,我们的逻辑仅仅是:获取当前用户 ID -> 封装 Blog 对象 -> 存入 MySQL 数据库。

现在引入了 Feed 流,业务流程必须向后延伸:

  1. 保存笔记到 MySQL(原逻辑保持不变,确保数据持久化)。

  2. 查询粉丝列表:tb_follow 关注表中,查出所有关注了当前博主(当前登录用户)的粉丝 ID。

  3. 循环推送(写扩散): 遍历这些粉丝 ID,将刚刚保存成功的笔记 ID,逐个存入每个粉丝专属的 Redis ZSet 收件箱中。

    • Key 设计: feed:{fanId}(例如:feed:1001 代表用户 1001 的收件箱)。
    • Value: 笔记的 blogId
    • Score: 当前的时间戳(利用时间戳的天然递增特性,让 ZSet 自动按照时间先后顺序排好序)。

二、 核心代码落地

我们需要来到 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 阻塞),导致前端用户点击“发布”后,页面卡住几秒甚至十几秒转圈圈,体验极差。

✅ 满分优化方案(你可以不写代码,但一定要知道思路):

  1. 异步解耦: 引入我们在秒杀优化中用到过的消息队列 (MQ) 或者 Spring 的 @Async 异步线程池。
  2. 逻辑剥离: 主线程只要执行完第 2 步(存入 MySQL),就立刻给前端返回“发布成功”。
  3. 后台搬砖: 把“查粉丝 -> 循环推 Redis”的脏活累活,扔给后台异步线程去慢慢做。反正粉丝晚几秒钟看到新动态,是完全可以接受的(这叫“最终一致性”)。

学习总结与终极预警

这一节,我们彻底打通了**“博主发件 -> 粉丝收件箱”**的数据上行链路。所有的动态现在已经安安静静地躺在粉丝的 Redis ZSet 里,并且按时间排好序了。


⚠️ 终极大挑战预警:

数据推进去了,怎么把它读出来?

你可能会说:“用 ZRANGE 或者 ZREVRANGE 查出来不就行了吗?”

如果这是一个静态不变的列表,确实可以。但是 Feed 流是时刻在动态新增的!

如果你用传统的 LIMIT offset, size (通过下标) 来进行分页查询,在用户不断往下滑动页面的过程中,如果有新的动态插入到了列表头部,整个列表的下标都会向后错位,导致用户刷到重复的帖子

为了解决动态列表的重复读取问题,我们必须引入一种极其硬核的分页思想——滚动分页 (Scroll Pagination)