什么是Feed流?
关注推送也叫做Feed流,直译为投喂。为用户持续的提供“沉浸式”的体验,通过无限下拉刷新获取新的信息。
Feed流和传统模式的区别如下

Feed流主要有三种实现方案
拉模式
又叫做读扩散,在用户需要读取消息时,主动从发件箱拉取消息,然后把获取到的消息进行排序,并呈现给用户。流程如图:

优点:消息只用保存一份,节约内存
缺点:大量消息被拉去的话,用户的性能消耗较大
推模式
又叫做写扩散,用户发布消息后,就直接推送到粉丝的收件箱里。流程如图:

优点:延时低,用户可以很快以较低成本获取到信息
缺点:对于内存的负担大,因为一份消息需要被存储很多份
推拉结合模式
又叫做读写混合,兼具两种模式的优点。对于普通粉丝,采用拉模式;对于活跃粉丝,采用推模式。如图所示:

三种模式对比
|
拉模式 |
推模式 |
推拉结合 |
写比例 |
低 |
高 |
中 |
读比例 |
高 |
低 |
中 |
用户读取延迟 |
高 |
低 |
低 |
实现难度 |
复杂 |
简单 |
很复杂 |
使用场景 |
很少使用 |
用户量少、没有大V |
过千万的用户量,有大V |
基于Redis实现推模式
需求
- 在保存blog到数据库的同时,推送到粉丝的收件箱
- 收件箱满足可以根据时间戳排序
- 查询收件箱数据时,可以分页查询
推送的具体实现
用户发布博客后,在保存到数据库的同时,保存到Redis中的ZSET中,key是每个粉丝的id。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @Override @Transactional public Long saveBlog(Blog blog) { UserDTO user = UserHolder.getUser(); blog.setUserId(user.getId()); boolean success = save(blog); if (!success) { throw new MySqlException(MessageConstant.BLOG_FAILED); } List<Follow> follows = followService.lambdaQuery().eq(Follow::getFollowUserId, user.getId()).list(); follows.forEach(follow -> { String key = RedisConstants.FEED_KEY + follow.getUserId(); redisTemplate.opsForZSet().add(key, blog.getId().toString(), System.currentTimeMillis()); }); return blog.getId(); }
|
Feed流的分页实现
Feed流中的数据会不断更新,所以数据的角标也在变化,因此不能采用传统的分页模式。所以我们在这里采用滚动分页来实现分页查询。

滚动分页查询需要的Redis命令
1
| ZREVRANGEBYSCORE key max min WITHSCORES LIMIT offset count
|
参数含义:
key
:ZSET的键名
max
:第一页为当前时间戳,后面的查询为上次查询的最小时间戳
min
:0
offset
:第一页为0,后面是上次查询时与最小值一样的元素个数
count
:每一页的数据条数
基于这条指令,实现业务代码。
先创建一个返回数据的DTO
1 2 3 4 5
| public class ScrollResult { private List<?> list; private Long minTime; private Integer offset; }
|
然后按照以下步骤实现业务逻辑:
- 获取当前用户
- 查询收件箱
- 解析数据:
blogId
,minTime
,offset
- 根据
blogId
查询博客
- 封装并返回
代码实现
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
| @Override public ScrollResult queryBlogOfFollow(Long max, Integer offset) { Long userId = UserHolder.getUser().getId(); String key = RedisConstants.FEED_KEY + userId; Set<ZSetOperations.TypedTuple<String>> typedTuples = redisTemplate.opsForZSet().reverseRangeByScoreWithScores( key, 0, max, offset, RedisConstants.BLOG_COUNT); if (typedTuples == null || typedTuples.isEmpty()) { return null; } long minTime = System.currentTimeMillis(); int nextOffset = 0; List<Long> ids = new ArrayList<>(typedTuples.size()); for (ZSetOperations.TypedTuple<String> tuple : typedTuples){ ids.add(Long.valueOf(tuple.getValue())); long time = tuple.getScore().longValue(); if (minTime == time) { nextOffset++; }else { minTime = time; nextOffset = 0; } } String idStr = StrUtil.join(",", ids); List<Blog> blogs = lambdaQuery() .in(Blog::getId, ids) .last("order by field(id, " + idStr + ")") .list(); blogs.forEach(blog -> { setIsLiked(blog); queryBlogUser(blog); }); ScrollResult scrollResult = ScrollResult.builder() .minTime(minTime) .offset(nextOffset) .list(blogs) .build(); return scrollResult; }
|
以上,就是Feed流的推模式实现。以后可能会改进为推拉结合。