关注推送-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,确保数据库查询结果有序
相关推荐
菜菜小狗的学习笔记2 小时前
黑马程序员Redis--基础篇
数据库·redis·缓存
面对疾风叭!哈撒给3 小时前
Linux之docker-compose使用(redis、nginx、tdengine、java应用)
linux·redis·docker
不是株3 小时前
Redis(实战篇)
数据库·redis·缓存
Anastasiozzzz4 小时前
放弃原生 C 语言字符串:深度解析 Redis SDS 的设计艺术
数据库·redis·缓存
YDS8294 小时前
黑马点评 —— 缓存穿透和缓存击穿及其解决方案
spring boot·redis·缓存
爱吃烤鸡翅的酸菜鱼4 小时前
从抽象设计到落地实践:openJiuwen可插拔会话存储机制深度解析
人工智能·redis·ai·agent
努力学习的小廉4 小时前
redis学习笔记(八)—— C++ 操作 Redis
redis·笔记·学习
難釋懷12 小时前
Redis分片集群插槽原理
数据库·redis·缓存
ノBye~12 小时前
Centos7.6 Docker安装redis(带密码 + 持久化)
java·redis·docker