共同关注功能
@Override
public Result common(Long id) {
Long userId = UserHolder.getUser().getId();
String key1="follow:"+id;
String key2="follow:"+userId;
//求两个用户关注的交集就是共同关注
Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key2, key1);
if (intersect.isEmpty()||intersect==null){
return Result.ok(Collections.emptyList());
}
List<Long> list = intersect.stream().map(Long::valueOf).collect(Collectors.toList());
List<UserDTO> userdtos = userService.listByIds(list)
.stream()
.map(user -> BeanUtil.copyProperties(user, UserDTO.class))
.collect(Collectors.toList());
return Result.ok(userdtos);
}
Feed流推送
关注推送又叫Feed流,直译为投喂,为用户持续的提供沉浸式体验,如抖音、快手这类短视频APP,通过无限下滑获取最新信息。
传统的推送模式是用户寻找内容,而Feed模式是内容匹配用户
- 拉模式又叫读扩散
粉丝从博主那里拉取:博主把自己的推文放到自己的发件箱,用户从多个博主那里拉取,但是每个博主发的推文时间是乱序的,那么用户拉取每个博主的推文后还要进行时间排序,比较耗时。
- 推模式又叫写扩散
博主推给粉丝:博主把写到的推文的id推给粉丝的收件箱,粉丝刷新后会消费到博主新的推文id,进而根据id加载出新推文,但是如果博主粉丝有千百万,那么要把推文id推给千百万个粉丝收件箱,仍然会比较耗时。
推模式实现关注推送功能
Redis数据结构可以选用list或者zset,list是链表有下标,可以实现排序和分页,zset可以基于score进行排序,排序之后也可以实现分页查询,但是最终选取zset来实现,原因如下:
list要依赖角标,有重复读问题
参考下面的zset,我觉得list也可以利用lastId实现无重复读效果,只不过zset可以根据score排序并利用score的范围查询,就使用zset了
zset不依赖角标,依赖lastId,无重复读问题
zset的score排序后虽然有角标的效果,但我们是利用score值的范围查询来实现分页的,把score按从大到小排列,每次查询记住最小的score作为下次查询的最大的score,即 lastId
zset集合的元素并不是存进去就有序的,而是可以根据需要用score排序!
因此并不是集合最后一个元素就是最小的时间戳!
(详见 黑马点评 实战篇P9、P10)
修改新增Blog的代码
@Override
public Result saveBlog(Blog blog) {
UserDTO user = UserHolder.getUser();
blog.setUserId(user.getId());
// 保存探店博文
boolean save = save(blog);
if (save){
//保存成功则把博文id发给粉丝,实现消息推送
//查询该用户的粉丝
List<Follow> followUsers = followService.query().eq("follow_user_id", user.getId()).list();
//把blogId推给每个粉丝的收件箱,用zset集合实现,时间戳作为score,这样就可以根据时间戳进行排序
for(Follow follow:followUsers){
Long userId = follow.getUserId();
String key="feed:"+userId;
stringRedisTemplate.opsForZSet().add(key,blog.getId().toString(), System.currentTimeMillis());
}
}
return Result.ok(blog.getId());
}
滚动分页查询
偏移量offset
假设有一个有序集合,分数从高到低排序为 [70, 60, 50, 40, 30, 20, 10],每次查询的偏移量为上次查询结果中最小分数相同的元素的个数。如果上一次查询的结果是 [50, 40, 30],那么下一次查询的偏移量就是 3,即上一次查询结果中最小分数相同的元素的个数,这样可以确保不会重复返回已经获取过的元素。
Controller层接口
@GetMapping("/of/follow")
public Result queryBlogOfFollow(
@RequestParam("lastId") Long max
,@RequestParam(value = "offset" ,defaultValue = "0") Integer offset) {
return blogService.queryBlogOfFollow(max, offset);
}
BlogServiceImpl实现
@Override
public Result queryBlogOfFollow(Long max, Integer offset) {
//获取当前登录用户的id
Long userId = UserHolder.getUser().getId();
String key="feed:"+userId;
//根据登录用户id去redis查询其收件箱得到推送的blog的id
//详见黑马点评 实战篇p10
Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate
.opsForZSet()
.reverseRangeByScoreWithScores(key, Double.valueOf(0), Double.valueOf(max), Long.valueOf(offset), Long.valueOf(2));
if (typedTuples==null||typedTuples.isEmpty())
{
return Result.ok(Collections.emptyList());
}
ArrayList<Long> IDlist = new ArrayList<>(typedTuples.size());
long minTime=0;
int os=1;
for (ZSetOperations.TypedTuple<String> tuple:typedTuples){
//获取id
String id = tuple.getValue();
IDlist.add(Long.valueOf(id));
long time = tuple.getScore().longValue();
if (time==minTime){
os++;//偏移量,每次偏移量都会变化,第一次偏移量默认是0,后面每次后端都要把查询后的偏移量传给前端
}else {
minTime=time;
os=1;//重置
}
}
String join = StrUtil.join(",", IDlist);
List<Blog> blogList = query().in("id", IDlist).last("ORDER BY FIELD(id," + join + ")").list();
for (Blog blog:blogList){
//查询blog有关的用户信息
Long id = blog.getUserId();
User user = userService.getById(id);
blog.setName(user.getNickName());
blog.setIcon(user.getIcon());
//判断该登录用户是否点赞
Long loginUserId = UserHolder.getUser().getId();
String k="blog:liked:"+id;
Double score = stringRedisTemplate.opsForZSet().score(k, loginUserId.toString());
blog.setIsLike(score!=null);
}
ScrollResult scrollResult = new ScrollResult();
scrollResult.setList(blogList);
scrollResult.setOffset(os);
scrollResult.setMinTime(minTime);
return Result.ok(scrollResult);
}
ScrollResult.java
@Data
public class ScrollResult {
private List<?> list;
//时间戳
private Long minTime;
//查询偏移量
private Integer offset;
}