介绍
关注推送也叫做Feed流,直译为投喂。为用户持续的提供"沉浸式"的体验,通过无限下拉刷新获取新的信息。
Feed流产品有两种常见模式:
- Timeline:不做内容筛选,简单的按照内容发布时间排序,常用于好友或关注。例如朋友圈
- 优点:信息全面,不会有缺失。并且实现也相对简单
- 缺点:信息噪音较多,用户不一定感兴趣,内容获取效率低
- 智能排序:利用智能算法屏蔽掉违规的、用户不感兴趣的内容。推送用户感兴趣信息来吸引用户
- 优点:投喂用户感兴趣信息,用户粘度很高,容易沉迷
- 缺点:如果算法不精准,可能起到反作用
本例中的个人页面,是基于关注的好友来做Feed流,因此采用Timeline的模式。该模式的实现方案有三种:
- 拉模式(读扩散)
- 推模式(写扩散)
- 推拉结合(读写混合):兼具推和拉两种模式的优点。
三种模式的详情以及对比可看:实战篇-07.好友关注-Feed流实现方案分析_哔哩哔哩_bilibili
本文是通过推模式来实现关注推送功能
推送实现
java
@Override
public Result saveBlog(Blog blog) {
// 获取登录用户
UserDTO user = UserHolder.getUser();
blog.setUserId(user.getId());
// 保存探店博文
boolean isSuccess = save(blog);
if(!isSuccess) {
return Result.fail("新增笔记失败!");
}
// 查询笔记作者的所有粉丝
List<Follow> follows = followService.query().eq("follow_user_id", user.getId()).list();
// 推送笔记id给所有粉丝
for (Follow follow : follows) {
// 获取粉丝id
Long userId = follow.getUserId();
// 推送
String key = "feed:" + userId;
stringRedisTemplate.opsForZSet().add(key, blog.getId().toString(), System.currentTimeMillis());
}
// 返回id
return Result.ok(blog.getId());
}
逻辑并不复杂
滚动分页实现
分页问题
Feed流中的数据会不断更新,所以数据的角标也在变化,因此不能采用传统的分页模式,而是采用滚动分页的方式
详见:实战篇-09.好友关注-滚动分页查询收件箱的思路_哔哩哔哩_bilibili
代码实现
java
@Override
public Result queryBlogOfFollow(Long max, Integer offset) {
// 获取当前用户
Long userId = UserHolder.getUser().getId();
// 查询收件箱
String key = RedisConstants.FEED_KEY + userId;
Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet()
.reverseRangeByScoreWithScores(key, 0, max, offset, 2);
if (typedTuples == null || typedTuples.isEmpty())
return Result.ok();
// 解析数据
List<Long> ids = new ArrayList<>(typedTuples.size());
long minTime = 0;
int os=1;
for (ZSetOperations.TypedTuple<String> typedTuple : typedTuples) {
// 获取笔记id
ids.add(Long.valueOf(typedTuple.getValue()));
// 获取分数
long time = typedTuple.getScore().longValue();
if(time == minTime)
os++;
else {
os=1;
minTime = time;
}
}
// 根据id查询笔记
List<Blog> blogs = query().in("id", ids).last("ORDER BY FIELD(id," + StrUtil.join(",", ids) + ")").list();
blogs.forEach(blog -> {
// 查询用户
queryBlogUser(blog);
// 查询blog是否被点赞
isBlogLiked(blog);
});
// 封装并返回
ScrollResult scrollResult = new ScrollResult();
scrollResult.setList(blogs);
scrollResult.setMinTime(minTime);
scrollResult.setOffset(os);
return Result.ok(scrollResult);
}
请求参数
max:上一次查询的最小时间戳
offset:偏移量
返回值里scrollResult会把当前查询后的max和offset返回给前端,下一次查询的参数就是通过这次的返回值得到的