Feed流实现方案
我们关注了博主之后,当用户发布了动态后我们应该把这些数据推送给粉丝,关注推送也叫作Feed(投喂)流
,通过无限下拉刷新获取新的信息
传统的模式内容检索
: 粉丝需要主动通过搜索引擎或者是其他方式去查找想看的内容新型Feed流的效果
: 系统分析用户到底想看什么,然后主动把内容推送给用户, 不需要用户主动去搜索资源节约用户时间
Feed流实现的两种模式
Timeline
: 对发布的信息不做内容筛选,简单的按照内容发布时间排序,常用于好友或关注(朋友圈等)
-
优点:信息全面不会有缺失并且实现也相对简单
-
缺点:信息噪音较多用户不一定感兴趣并且内容获取效率低
智能排序
:利用智能算法屏蔽掉违规的、用户不感兴趣的内容,推送用户感兴趣的信息来吸引用户
- 优点: 投喂用户感兴趣的信息,用户粘度很高,容易沉迷
- 缺点: 如果算法不精准可能会起到反作用即给用户推了不想看的内容
个人页面一般就是基于关注的好友来做Feed流,因此采用Timeline
方式只需要拿到我们关注用户的发布的内容然后按照时间排序
即可
拉模式
也叫读扩散,每个用户都有自己的发件箱和收件箱
优点节约空间
:系统只会将用户关注的博主发布的动态放入到用户的收件箱,用户每次从自己的收件箱中读取信息缺点拉取信息有延迟
:只要用户读取数据时就会去他关注的所有人的发件箱中拉取信息,假设该用户关注了海量用户此时就会拉取很多信息对服务器压力巨大
推模式
也叫写扩散,推模式没有发件箱
优点时效快
: 系统主动将博主发布的动态推送到其粉丝的收件箱中,这样用户每次读取信息时就不需要临时拉取缺点
: 内存压力大,假设一个大V发了一个动态此时就会写很多份数据发到他的粉丝收件箱中
推拉结合
也叫读写混合,兼具推和拉两种模式的优点,只要大v才有发件箱
推送笔记ID到粉丝收件箱
需求: 修改新增探店笔记的业务,在保存Blog对象到数据库的同时推送笔记到粉丝的收件箱
实现收件箱功能
: 收件箱要求满足可以根据时间戳对数据排序(降序),同时用户查询收件箱数据时也可以根据角标实现分页查询
传统的分页模式
: 查询数据时要求数据库中的数据角标必须固定,否则可能会出现数据重复读取的情况
Feed流的滚动分页
: 记录每次查询操作的最后一条数据,下次查询时会从上次读取的最后一条数据之后开始读取数据
List集合
: 只能按照角标查询所以不支持滚动分页SortedSet集合
: 可以按照集合中元素的score值的范围进行查询,我们每次查询的时候可以记录查询到笔记Id的最小时间戳
第一步: 修改BlogController
中保存笔记的方法,当博主发布完探店笔记后还要将发布笔记的Id推送到所有粉丝的收件箱中,score值是当前的时间戳(默认升序)
// 在RedsiConstants类声明一个常量作为用户收件箱的前缀
public static final String FEED_KEY = "feed:";
@Resource
private IFollowService followService;
@Override
public Result saveBlog(Blog blog) {
// 获取登录用户
UserDTO user = UserHolder.getUser();
blog.setUserId(user.getId());
// 保存探店博文
boolean isSuccess = save(blog);
if(!isSuccess){
return Result.fail("新增笔记失败");
}
// 条件构造器
LambdaQueryWrapper<Follow> queryWrapper = new LambdaQueryWrapper<>();
// 从follow表中查找博主的所有粉丝select * from follow where follow_user_id = user_id
queryWrapper.eq(Follow::getFollowUserId, user.getId());
List<Follow> follows = followService.list(queryWrapper);
for (Follow follow : follows) {
// 获取粉丝Id
Long userId = follow.getUserId();
// 推送发布笔记的Id到每个粉丝的收件箱(score值是当前时间戳),每个粉丝都有一个自己的收件箱
String key = FEED_KEY + userId;
stringRedisTemplate.opsForZSet().add(key, blog.getId().toString(), System.currentTimeMillis());
}
// 返回笔记的id
return Result.ok(blog.getId());
}
查询推送笔记
需求: 在个人主页的关注栏中查询并展示推送的Blog信息
ZREVRANGEBYSCORE key 最大值范围 最小值范围 WITHSCORES LIMIT offSet(偏移量) 查询到的个数
: 获取指定的score范围内的元素并按照降序排序
最小时间戳
: 每次分页查询完成之后要记录查询的最小时间戳,将这个最小时间戳作为下一次分页查询的条件编译量
: 设置要从上次查询的最大值后面跳过几个元素,0表示不跳过元素,1表示跳过一个元素,偏移量的值取决于当前集合内有几个元素和上次查询到的最大值相同
第一步: 业务中不一定只对Blog
进行分页查询,可以使用泛型做一个通用的分页查询
@Data
public class ScrollResult {
// 封装查询到的数据
private List<?> list;
// 记录本次查询的最小时间戳,作为下一次查询的最大值(起始值)
private Long minTime;
// 记录偏移量
private Integer offset;
}
第二步: 在BlogController
中创建对应的分页查询方法, 具体实现逻辑在BlogServiceImpl
中完成
@GetMapping("/of/follow")
// 由于第一次查询的时候没有传递offset参数,可以设置默认值为0
public Result queryBlogOfFollow(
@RequestParam("lastId") Long max, @RequestParam(value = "offset",defaultValue = "0") Integer offset){
return blogService.queryBlogOfFollow(max,offset);
}
@Override
public Result queryBlogOfFollow(Long max, Integer offset) {
//1. 获取当前用户的Id
Long userId = UserHolder.getUser().getId();
//2. 查询该用户的收件箱获取该用户对应的SortedSet集合中所有的笔记Id及score值看是否有关注的博主发了笔记
String key = FEED_KEY + userId;
Set<ZSetOperations.TypedTuple<String>> typeTuples = stringRedisTemplate.opsForZSet()
.reverseRangeByScoreWithScores(key, 0, max, offset, 2);
//3. 非空判断
if (typeTuples == null || typeTuples.isEmpty()){
return Result.ok(Collections.emptyList());
}
//4. 获取SortedSet集合中所有的笔记Id及score值,List集合的大小和Set集合一致,可以略微提高效率避免长度重置
ArrayList<Long> ids = new ArrayList<>(typeTuples.size());
// 保存最小的时间戳
long minTime = 0;
// 记录最小时间戳的个数即偏移量
int os = 1;
for (ZSetOperations.TypedTuple<String> typeTuple : typeTuples) {
//4.1 获取推送的Blog的Id
String id = typeTuple.getValue();
ids.add(Long.valueOf(id));
//4.2 将笔记对应的score(时间戳)转换为long类型
long time = typeTuple.getScore().longValue();
if (time == minTime){// 如果当前的时间戳等于最小时间戳则最小时间戳个数+1
os++;
}else {// 如果当前时间戳不等于最小时间戳,则把当前时间作为最小时间戳同时把最小时间戳的个数重置为1
minTime = time;
os = 1;
}
}
// 解决MySQL的in语句的自动按照id大小的排序问题,手动指定排序方式为传入的ids集合中的顺序
String idsStr = StrUtil.join(",");
//5. 根据推送的ids集合查询所有的blog
List<Blog> blogs = query().in("id", ids).last("ORDER BY FIELD(id," + idsStr + ")").list()
for (Blog blog : blogs) {
//5.1 查询发布该blog的用户信息
queryBlogUser(blog);
//5.2 查询当前用户是否给该blog点过赞
isBlogLiked(blog);
}
//6. 封装结果并返回
ScrollResult scrollResult = new ScrollResult();
scrollResult.setList(blogs);
scrollResult.setOffset(os);
scrollResult.setMinTime(minTime);
return Result.ok(scrollResult);
}