1. Feed流实现方案
拉模式主要缺点,延迟问题,极端情况某个用户关注了成千上万的up主,每位up主又发布了十几条博客,此时拉模式的延迟就会很高;
推模式缺点也很明显,内存消耗太大,假设up主是千万级别的,此时往每个粉丝的收件箱都要发送推文,收件箱要保存上亿份,消耗的内存太大;
对于普通的up主采用的是推模式,也就是直接发送给粉丝的收件箱,而自己没有发件箱;
而对于大V,有两种情况,如果是活跃度高的粉丝,采用的就是推模式,会直接获取信息,延时低;而对于那种普通不活跃或者僵尸粉,则采用的是拉模式,用户需要从发件箱中获取,延时会高一点;
一般达不到千万级别以上都是采用推模式,所以案例采用推模式,难度简单;
2. 基于推模式实现关注推送功能
采用SortedSet数据结构
选择SortedSet原因:
- 对于变化的排名进行分页时采用SortedSet;
- 数据动态变化;
- 排行榜;
传统分页模式缺点:读取到重复数据
滚动分页实现:记录每一次查询的最后一条
/**
* 保存博客,并且推送到粉丝的收件箱中
* @param blog
* @return
*/
@Override
public Result saveBlog(Blog blog) {
// 1.获取当前用户
UserDTO user = UserHolder.getUser();
blog.setUserId(user.getId());
// 2.保存探店笔记
boolean isSuccess = save(blog);
if(!isSuccess){
return Result.fail("保存失败");
}
// 3.推送笔记给粉丝,所以首先需要获取当前用户的粉丝列表
// user_id:用户id follow_user_id:被关注的博主的id
List<Follow> followUserIds = followService.query().eq("follow_user_id", user.getId()).list();
// 4.遍历整个粉丝列表,推送给粉丝
for (Follow follow:followUserIds) {
// 4.1 获取粉丝id
Long userId = follow.getUserId();
// 4.2 推送笔记id给所有粉丝,key值为用户自身的id
String key = "feed:" + userId;
stringRedisTemplate.opsForZSet().add(key, blog.getId().toString(), System.currentTimeMillis());
}
//5. 返回id
return Result.ok(blog.getId());
}
3. 实现关注推送页面的分页查询
重点是如何解决滚动分页查询收件箱
如果按角标(value)查,角标变化会出现重复查询;
按照score值查,记录查询最小score;
测试时max取1000,实际业务时取当前时间为最大值;
限制为查询条数,offset表示偏移量,意思是上一条查询中,和最小值一样的条数,避免重复(一般取值为1,因为也不需要查询上一条查询中最小值);
count表示查询的条数;
偏移量继续设为0则会重复查询;
代码实现
第一次查询给默认值,时间戳为当前时间,偏移量为0
之后的值有后续代码给出;
返回值类:
@Data
public class ScrollResult {
private List<?> list;
private Long minTime;
private Integer offset;
}
实现类,见注释
/**
* 滚动查询,展示博主推送的笔记,新发布的滚动查询查不到,但是往上滚,前端做了处理,就是刷新重新查询,开始位置在当前最新位置
* @param max
* @param offset
* @return
*/
@Override
public Result queryBlogOfFollow(Long max, Integer offset) {
//1.找到用户
UserDTO user = UserHolder.getUser();
//2.查询用户收件箱
String key = FEED_KEY + user.getId();
Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet()
.rangeByScoreWithScores(key, 0, max, offset, 2);
//判空操作
if(typedTuples == null || typedTuples.isEmpty()) {
return Result.ok();
}
//3.解析收件箱数据:blogId、minTime(时间戳)、offset(偏移量)
ArrayList<Object> ids = new ArrayList<>(typedTuples.size());
long minTime = 0; //这个minTime是上次查询的最小时间戳,作为当次查询的最大时间戳来开始查
int os = 1; //计算分数值等于最小值的个数
for (ZSetOperations.TypedTuple<String> typedTuple:typedTuples) {
//获取博客id转换为Long型并存入ids数组中
//获取id
String id = typedTuple.getValue();
ids.add(Long.valueOf(id));
//获取分数(时间戳)
long time = typedTuple.getScore().longValue();
if(time == minTime){
os += 1;
}else {
minTime = time;
os = 1;
}
}
//4.根据id查询blog,采用mp的批量查询
//但是由于用mp提供的listByIds是用in方法查,不能保证顺序
//需要加orderby语句,需要id字符串
//使用last方法添加查询条件,按照给定的ID顺序进行排序。idStr是一个字符串,用于指定ID的顺序。
String idStr = StrUtil.join(",", ids);
List<Blog> blogList = query().in("id", ids).last("order by field(id," + idStr + ")").list();
//其中每个blog都要做查询该blog博主信息和点赞信息的操作
for (Blog blog:blogList) {
//查询blog有关的用户
queryBlogUser(blog);
//查询blog是否被点赞
isBlogLiked(blog);
}
//5.封装并返回
ScrollResult r = new ScrollResult();
r.setList(blogList);
r.setOffset(os);
r.setMinTime(minTime);
return Result.ok(r);
}