Feed流简单理解

简单理解:Feed流 → 推送流 → 下拉(上拉)刷新

两种模式:

  1. Timeline: 不做内容筛选,简单的按照内容发布时间排序,常用于好友或关注。例如朋友圈
  2. 智能排序:利用智能算法屏蔽掉违规的、用户不感兴趣的内容。推送用户感兴趣信息来吸引用户

1. 实现Timeline的三种方式

以用户查看关注的博主更新为例

  1. 推模式:将博主更新的博客推送到用户收件箱

  2. 拉模式:当用户查看时,从关注的博主的发件箱获取博文,按照时间排序等处理后,发送到用户收件箱

  3. 推拉结合:使用算法,对于cold用户采用拉模式,对于hot用户采用推模式

2. 使用推模式实现关注博主博文推送

2.1 需求分析

  • 在保存博文到数据库的同时,需要将其推送到用户收件箱中;
  • 收件箱满足可以根据时间戳排序,必须用Redis的数据结构实现
  • 查询收件箱数据时,可以实现分页查询

存在的问题:

Feed流中的数据会不断更新,所以数据的角标也在变化,因此不能采用传统的分页模式。

问题分析:

假设在t1 时刻,我们去读取第一页,此时page = 1 ,size = 5 ,那么我们拿到的就是10~6 这几条记录,假设现在t2时候又发布了一条记录,此时t3 时刻,我们来读取第二页,读取第二页传入的参数是page=2 ,size=5 ,那么此时读取到的第二页实际上是从6 开始,然后是6~2 ,那么我们就读取到了重复的数据,所以feed流的分页,不能采用原始方案来做。

1653813047671

解决方案:

记录每次操作的最后一条,然后从这个位置开始去读取数据。我们从t1时刻开始,拿第一页数据,拿到了10~6,然后记录下当前最后一次拿取的记录,就是6,t2时刻发布了新的记录,此时这个11放到最顶上,但是不会影响我们之前记录的6,此时t3时刻来拿第二页,第二页这个时候拿数据,还是从6后一点的5去拿,就拿到了5-1的记录。我们这个地方可以采用sortedSet来做,可以进行范围查询,并且还可以记录当前获取数据时间戳最小值,就可以实现滚动分页了。

1653813462834

2.2 实现推送

@Override  
public Result saveBlog(Blog blog) {  
    // 1. 获取登录用户  
    UserDTO user = UserHolder.getUser();  
    blog.setUserId(user.getId());  
    // 2. 保存探店博文  
    save(blog);  
    // 3. 查询关注该博主的用户  
    List<Follow> followers = followService.query().eq("follow_user_id", user.getId()).list();  
    // 4. 推送博文到粉丝的收件箱  
    for (Follow follower : followers) {  
        String key = FEED_KEY + follower.getId();  
        stringRedisTemplate.opsForZSet().add(key, blog.getId().toString(), System.currentTimeMillis());  
    }  
    // 5. 返回id  
    return Result.ok(blog.getId());  
}

2.3 实现分页查询收件箱

难点分析:

  • 分析查询出的数据中的最小时间戳,作为下一次查询的最大值
  • 找到查询数据中与最小时间错相同的数据个数,作为下次查询的偏移量
  1. 定义具体的返回值实体类
@Data  
public class ScrollResult {  
    private List<?> list;  // 采用泛型,实现通用
    private Long minTime;  
    private Integer offset;  
}
  1. 定义接口
@GetMapping("/of/follow")  
public Result queryBlogOfFollow(@RequestParam("lastId") Long max, @RequestParam(value = "offset", defaultValue = "0") Integer offset){  
    return blogService.queryBlogOfFollow(max, offset);  
}
  1. 业务层逻辑
@Override  
public Result queryBlogOfFollow(Long max, Integer offset) {  
    // 1. 获取当前用户  
    Long userId = UserHolder.getUser().getId();  
    // 2. 查询收件箱 ZREVRANGEBYSCORE KEY MAX MIN LIMIT OFFSET COUNT    String key = FEED_KEY + userId;  
    Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate  
            .opsForZSet()  
            .reverseRangeByScoreWithScores(key, 0, max, offset, 2);  
    // 3. 非空判断  
    if (typedTuples == null || typedTuples.isEmpty()){  
        return Result.ok();  
    }  
  
    // 4. 解析数据:blogId,最小时间戳, offset  
    ArrayList<Long> idList = new ArrayList<>(typedTuples.size());  
    long minTime = 0; // 最小时间戳  
    Integer nextOffset = 1; // 偏移量  
    for (ZSetOperations.TypedTuple<String> typedTuple : typedTuples) {  
        // 4.1 获取id  
        idList.add(Long.valueOf(typedTuple.getValue()));  
        // 4.2 获取分数(时间戳)  
        long time = typedTuple.getScore().longValue();  
        if (time == minTime) {  
            nextOffset++;  
        } else {  
            minTime = time;  
            nextOffset = 1;  
        }  
    }  
    nextOffset = minTime == max ? nextOffset : offset + nextOffset;  
    String idListStr = StrUtil.join(",", idList);  
    List<Blog> blogList = query().in("id", idList).last("order by field(id, " + idListStr + ")").list();  
  
    for (Blog blog : blogList) {  
        // 5.1 查询blog的作者  
        queryBlogUser(blog);  
        // 5.2 查询当前用户是否点赞  
        isBlogLiked(blog);  
    }  
  
    // 6. 封装ScrollResult,并返回  
    ScrollResult scrollResult = new ScrollResult(blogList, minTime, nextOffset);  
    return Result.ok(scrollResult);  
}