目录
实现关注与取关
实现
Controller层
@RestController
@RequestMapping("/follow")
public class FollowController {
@Resource
private IFollowService followService;
@PutMapping("/{id}/{isFollow}")
public Result follow(@PathVariable("id")Long followUserId,@PathVariable("isFollow") Boolean isFollow){
return followService.follow(followUserId,isFollow);
}
@GetMapping("/or/not/{id}")
public Result follow(@PathVariable("id")Long followUserId){
return followService.isFollow(followUserId);
}
}
Service层
@Override
public Result follow(Long followUserId, Boolean isFollow) {
//1.获取登录用户
UserDTO user = UserHolder.getUser();
Long userId = user.getId();
//判断是取关还是关注
if(isFollow){
//2.关注,新增数据
Follow follow = new Follow();
follow.setUserId(userId);
follow.setFollowUserId(followUserId);
follow.setCreateTime(LocalDateTime.now());
boolean isSuccess = save(follow);
}
else{
//取关,删除数据
//sql: delete from tb_follow where user_id = ? and follow_user_id = ?
remove(new QueryWrapper<Follow>()
.eq("user_id",userId)
.eq("follow_user_id",followUserId)
);
}
return Result.ok();
}
@Override
public Result isFollow(Long followUserId) {
//1.获取用户
UserDTO user = UserHolder.getUser();
Long userId = user.getId();
//查询是否关注
//sql: select * from tb_follow where user_id = ? and follow_user_id
Integer count = query().eq("user_id", userId).eq("follow_user_id", followUserId).count();
return Result.ok(count>0);
}
实现共同关注功能
修改前面的关注接口,关注的对象不仅仅存入数据库中,还需存入redis中(共同关注功能需要set数据结构完成)
修改
@Override
public Result follow(Long followUserId, Boolean isFollow) {
//1.获取登录用户
UserDTO user = UserHolder.getUser();
Long userId = user.getId();
String key = "follows:"+userId;
//判断是取关还是关注
if(isFollow){
//2.关注,新增数据
Follow follow = new Follow();
follow.setUserId(userId);
follow.setFollowUserId(followUserId);
follow.setCreateTime(LocalDateTime.now());
//修改部分
boolean isSuccess = save(follow);
if(isSuccess){
//按关注用户的id放入redis的set集合中
stringRedisTemplate.opsForSet().add(key,followUserId.toString());
}
}
else{
//取关,删除数据
//sql: delete from tb_follow where user_id = ? and follow_user_id = ?
//修改部分
boolean isSuccess = remove(new QueryWrapper<Follow>()
.eq("user_id", userId)
.eq("follow_user_id", followUserId)
);
if(isSuccess){
stringRedisTemplate.opsForSet().remove(key,followUserId.toString());
}
}
return Result.ok();
}
实现
Controller层
@GetMapping("/common/{id}")
public Result followCommons(@PathVariable("id")Long id){
return followService.followCommons(id);
}
Service层
@Override
public Result followCommons(Long id) {
//1.获取当前用户id
Long userId = UserHolder.getUser().getId();
String key1 = "follows:"+userId;
//2.求交集
String key2= "follows:"+id;
Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key1, key2);
if(intersect==null||intersect.isEmpty()){
//无共同关注
return Result.ok(Collections.emptyList());
}
//解析id集合
List<Long> ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList());
//查询用户
List<UserDTO> users = userService.listByIds(ids)
.stream()
.map(user -> BeanUtil.copyProperties(user, UserDTO.class))
.collect(Collectors.toList());
return Result.ok(users);
}
关注推送
Feed流的模式
拉模式
(msg后面数据为时间戳)如下所示,赵六收件箱一般是空的,只有在赵六在读消息时才会来拉取他关注目标的发件箱,并按照时间排序
优点:节省内存空间,收件箱读完以后,就不用了可以清理,所以消息只保存一份在发件人的发件箱中
缺点:每次去读消息时,都要去拉取再做排序,耗时时间长,读取时间较
推模式
弥补拉模式的缺点,每次都会把消息发给每一个粉丝,并排序,等粉丝读取
缺点:这种扩散方式,每次更新消息时,都会发给每个粉丝,所以内存占用率很高
推拉结合模式
普通人采取推模式
大V,活跃粉丝采用推模式,普通的粉丝采用拉模式
总结
基于推模式实现关注推送功能
Feed流的分页问题
思考应该用Redis中哪种数据结构进行时间戳的排序?是sortedset还是list?
list查询数据只能按照角标查询,或者首尾查询,不支持滚动分页
sortedset按照score值排序,得出排名,如果按照排名查询则跟角标查询无区别,但是sortedset支持按照score值范围进行查询(把时间戳按照从大到小进行排列,每次排序都记住最小的时间戳,然后下次查询时再找比这个时间戳更小的,这样就实现了滚动分页查询)
小结:如果数据会有变化,尽量使用sortedset实现分页查询
采用传统角标查询会出现下列问题
使用滚动分页,每次存入页的最后一次角标
实现
Controller层
@PostMapping
public Result saveBlog(@RequestBody Blog blog) {
return blogService.saveBlog(blog);
}
Service层
@Override
public Result saveBlog(Blog blog) {
//获取登录用户
UserDTO user = UserHolder.getUser();
blog.setUserId(user.getId());
//2.保存笔记
boolean isSuccess = save(blog);
if(isSuccess == false){
return Result.fail("新增笔记失败!");
}
//3.查询笔记作者所有粉丝
//sql: select * from tb_follow where follow_user_id = ?
List<Follow> follows = followService.query().eq("follow_user_id", user.getId()).list();
//4.推送笔记id给所有粉丝
for (Follow follow : follows) {
//4.1获取粉丝id
Long userId = follow.getUserId();
//4.2推送sortedSet
String key = "feed:" + userId;
stringRedisTemplate.opsForZSet().add(key,blog.getId().toString(),System.currentTimeMillis());
}
//3.返回id
return Result.ok(blog.getId());
}
实现关注推送页面的分页查询
角标查询 和 通过记录最后一个的范围查询的对比
一开始redis中z1集合存入的数据("m6" : 6,"m5" : 5,"m4" : 4,"m3" : 3,"m2" : 2,"m1" : 1)
实现滚动分页查询一定要传入四个参数:
分数的最大值(max),分数的最小值(min),偏移量(offset),数量(count)(其中分数最小值,数量固定不变)
最大值每次都要找上一次查询的最小分数(除了第一次)
偏移量(第一次采取0,以后采取1,小于等于与小于的区别)
注意:如果分数一样会出现以下问题,所以offset应是上一次查询的最小分数的总个数
接口说明
Controller层
@GetMapping("/of/follow")
public Result queryBlogOfFollow(@RequestParam("lastId") Long max,@RequestParam(value = "offset",defaultValue = "0") Integer offset){
return blogService.queryBlogOfFollow(max,offset);
}
Service层
@Override
public Result queryBlogOfFollow(Long max, Integer offset) {
//1.获取当前用户
Long userId = UserHolder.getUser().getId();
//2.查询收件箱
//ZREVRANGEBYSCORE key Max Min LIMIT offset count
String key = "feed:" + userId;
//元组
Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet()
.reverseRangeByScoreWithScores(key, 0, max, offset, 2);
//非空判断
if(typedTuples==null||typedTuples.isEmpty()){
return Result.ok();
}
//3.解析数据:blogId、minTime(时间戳)、offset
List<Long> ids = new ArrayList<>(typedTuples.size());
long minTime = 0;
int os = 1;//偏移量
for (ZSetOperations.TypedTuple<String> typedTuple : typedTuples) {
//3.1获取id(blogId)
ids.add(Long.valueOf(typedTuple.getValue()));
//3.2获取分数(时间戳)
//执行到最一定是最小时间戳
long time = typedTuple.getScore().longValue();
if(time == minTime){
os++;
}
else{
minTime = time;
os = 1;
}
}
//4。根据id查询blog
//不能采用下面方法查询 in 语句无序
//List<Blog> blogs = listByIds(ids);
//应使用
String idStr = StrUtil.join(",", ids);
List<Blog> blogs = query().in("id", ids)
.last("ORDER BY FIELD(id," + idStr + ")")
.list();
//5.封装并返回
//需要将blog的信息完善
for (Blog blog : blogs) {
//查询blog相关用户
queryBlogUser(blog);
//查询blog是否被点赞
isBlogLiked(blog);
}
ScrollResult scrollResult = new ScrollResult();
scrollResult.setList(blogs);
scrollResult.setOffset(offset);
scrollResult.setMinTime(minTime);
return Result.ok(scrollResult);
}