什么是Feed流
完成了一个需求,展示关注的人发布的博文,这里面就涉及到Feed流系统的设计。Feed 流产品在我们手机APP中几乎无处不在,比如微信朋友圈、新浪微博、今日头条等。只要大拇指不停地往下划手机屏幕,就有一条条的信息不断涌现出来。就像给宠物喂食一样,只要它吃光了就要不断再往里加,故此得名Feed(饲养)。
定义
- Feed:Feed流中的每一条消息或者状态都是Feed,比如你微博关注的明星发布的微博,朋友圈朋友发的一篇文章
- Feed流:将用户主动订阅的若干消息源组合在一起形成内容聚合器,帮助用户持续地获取最新的订阅源内容
分类
- TimeLine:基于关注关系并按时间排列,按照Feed流内容更新的时间先后顺序,将内容展示给用户,早期的微博、朋友圈都是典型的timeline。
- Rank:基于算法推荐,按照某些因素计算内容的权重,以决定展示内容的先后顺序
Feed模式
push
推模式:作者发布新的动态时,要推送给其所有的粉丝。
优点:每次用户想要读取关注作者的文章都可以从自己的收件箱直接读取,读取延时低
缺点:内存占用高,一个作者的动态要写N份到其粉丝的收件箱中,逻辑复杂,粉丝数多的时候会是灾难
pull
拉模型:作者发布动态时只发送到自己的发件箱,用户拉取关注的每个作者的动态,聚合后展示
优点:拉模型不需要存储额外的数据,逻辑简单
缺点:每次用户想要读取关注作者的文章,都需要重新拉取,读取延迟较高,关注人数多的时候会出现灾难
推拉结合
基于推模式实现Feed流实战
使用Redis的ZSet作为粉丝的收件箱,使用时间戳对博文排序,Key为博文id
1 2 3 4 5 6 7
| List<Follow> followUsers = followService.query().eq("follow_user_id", user.getId()).list(); for (Follow follow : followUsers) { Long userId = follow.getUserId(); String key = FEED_KEY + userId; stringRedisTemplate.opsForZSet().add(key, blog.getId().toString(), System.currentTimeMillis()); }
|
粉丝在查询自己的收件箱时可能出现分页问题。Feed流本质上是一个动态列表,列表内容会随着时间不断变化。传统的前端分页参数使用page_size和page_num,分表表示每页几条,以及当前是第几页,对于静态列表没问题,但是对于一个动态列表,可能会出现分页时还有人新发布笔记的情况,如果继续按照数据的角标分页,就会导致重复展示之前页面展示过的数据。
比如当前列表blogId为(5、4、3、2、1),一页展示三条,第一页就是(5、4、3),此时发布了新的博客(id = 6)并推送到了Redis收件箱:(6、5、4、3、2、1),按照角标的话,第二页就会展示(3、2、1),重复查询了id = 3 的博客
所以这里采用上一页的最后一项(比如5、4、3、3、2、1,一页三条的话最后一项就是3)来标记下一页的起始位置,同时指定一个offset(这里有2个3)用来跳过已经在上一页末尾显示过的重复项
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
|
@Override public Result queryBlogFollow(Long max, Integer offset) { Long userId = UserHolder.getUser().getId(); String key = FEED_KEY + userId; Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet(). reverseRangeByScoreWithScores(key, 0, max, offset, 2); if (typedTuples == null || typedTuples.isEmpty()) { return Result.ok(Collections.emptyList()); } long minTime = 0; int offset1 = 1; List<Long> blogIds = new ArrayList<>(typedTuples.size()); for (ZSetOperations.TypedTuple<String> typedTuple : typedTuples) { blogIds.add(Long.valueOf(typedTuple.getValue())); long t = typedTuple.getScore().longValue(); if (t == minTime) { offset1 += 1; } else { minTime = t; offset1 = 1; } } String idStr = StrUtil.join(",", blogIds); List<Blog> blogs = query() .in("id", blogIds) .last("ORDER BY FIELD(id," + idStr + ")").list(); for (Blog blog : blogs) { updateBlogByUserInfo(blog); isBlogLiked(blog); } ScrollResult scrollResult = new ScrollResult(); scrollResult.setList(blogs); scrollResult.setOffset(offset1); scrollResult.setMinTime(minTime); return Result.ok(scrollResult); }
|