达人探店和好友关注功能(feed流的使用,滚动分页查询)
目录
- 达人探店
- 一:发布笔记
- 二:点赞功能
- 三:点赞排行
- 好友关注
- 一:关注和取关
- 二:共同关注
- 三:关注推送
- 1:feed流实现方案分析
- 2:基于推模式实现关注推送功能
- 3:滚动分页查询
达人探店
一:发布笔记
controller:
@GetMapping("/{id}")
public Result queryBlog(@PathVariable Long id){return blogService.queryBlog(id);
}
service:
@Override
public Result queryBlog(Long id) {Blog blog = getById(id);if (blog==null){return Result.fail("当前博客不存在!");}Long userId = blog.getUserId();User user = userService.getById(id);String nickName = user.getNickName();String icon = user.getIcon();blog.setIcon(icon);blog.setName(nickName);return Result.ok(blog);
}
二:点赞功能
@Override
@Transactional
public Result queryLike(Long id) {String key = RedisConstants.BLOG_LIKED_KEY + id;Long userId = UserHolder.getUser().getId();Long add = stringRedisTemplate.opsForSet().add(key, userId.toString());if (add == 0) {boolean ifSuccess = update().setSql("liked=liked-1").eq("id", id).update();if (BooleanUtil.isTrue(ifSuccess)) {stringRedisTemplate.opsForSet().remove(key, userId.toString());}} else {update().setSql("liked=liked+1").eq("id", id).update();}return Result.ok();
}
然后在根据id查询blog业务中,判断用户有没有点赞,对islike赋值
早blog的分页查询中,也是判断用户有没有点赞,然后对islike赋值;
供前端来响应体现是否高亮
三:点赞排行
点赞排行榜是在有多人点赞时,在点赞位置显示头像相关信息,而且是按照点赞顺序先后顺序进行排序,我们之前将点赞的用户全部存入了redis中的set集合中,想要取出按时间排序的是不行的,只有将他zset,score按照时间进行排序。
我们存入zset中时,以当前的时间戳作为score,这样越小的排在越前面,因为时间越小时间戳越小;
然后我们显示是否有没有点赞的时候需要判断当前用户有没有在zset中,因为zset中没有判断值是否存在的方法,只有返回score的方法,我们可以利用这个方法,score有值说明用户存在,为空用户不存在;
Long userId =user.getId();
Boolean add = stringRedisTemplate.opsForZSet().add(key, userId.toString(), System.currentTimeMillis());
if (BooleanUtil.isFalse(add)) {boolean ifSuccess = update().setSql("liked=liked-1").eq("id", id).update();if (BooleanUtil.isTrue(ifSuccess)) {stringRedisTemplate.opsForZSet().remove(key, userId.toString());}
} else {update().setSql("liked=liked+1").eq("id", id).update();
}
return Result.ok();
取出时:
Double member = stringRedisTemplate.opsForZSet().score(RedisConstants.BLOG_LIKED_KEY + id, UserHolder.getUser().getId().toString());
blog.setIsLike(member != null);
然后获取点赞用户的前五名,就要从集合中取:
@Override
public Result queryLikes(Long id) {String key=RedisConstants.BLOG_LIKED_KEY + id;//从set中获取排行榜前五的用户idSet<String> range = stringRedisTemplate.opsForZSet().range(key, 0, 4);if (range==null||range.isEmpty()){return Result.ok();}//使用流式编程将id取出且变成long类型的集合List<Long> collect = range.stream().map(Long::valueOf).collect(Collectors.toList());//将集合中的元素转成字符串并且用‘,’隔开;String join = StrUtil.join(",", collect);//调用mp的方法,根据id集合查询用户List<User> users = userService.query().in("id",collect).last("order by field(id,"+ join+")").list();//将用户转成用户dtoList<UserDTO> userDTOS = BeanUtil.copyToList(users, UserDTO.class);return Result.ok(userDTOS);
}
因为mysql的in()查询的顺序和我们传入的顺序是不一样的所以我们自己手写sql:
List<User> users = userService.query().in("id",collect).last("order by field(id,"+ join+")").list();
好友关注
一:关注和取关
关注:
@Override
public Result follow(Long id, Boolean isFollow) {Long userId = UserHolder.getUser().getId();Follow follow = new Follow();if (isFollow) {follow.setUserId(userId);follow.setFollowUserId(id);save(follow);}else {remove(new QueryWrapper<Follow>().eq("user_id",userId).eq("follow_user_id",id));}return Result.ok();
}
如果是关注就将数据存入follow中,如果是取关就将这条数据从follow中删除
判断是否关注:
@Override
public Result isFollow(Long id) {Long userId = UserHolder.getUser().getId();Long count = query().eq("user_id", userId).eq("follow_user_id", id).count();return Result.ok(count>0);
}
二:共同关注
共同关注的功能我们可以借助redis的set集合的求交集并集来求,所以在我们关注了别人或者,取关了别人时,要将数据从redis中取出或删除:
@Override
@Transactional
public Result follow(Long id, Boolean isFollow) {Long userId = UserHolder.getUser().getId();String key= "follows:"+userId;Follow follow = new Follow();if (isFollow) {follow.setUserId(userId);follow.setFollowUserId(id);save(follow);//存入redisstringRedisTemplate.opsForSet().add(key,id.toString());}else {remove(new QueryWrapper<Follow>().eq("user_id",userId).eq("follow_user_id",id));//从redis中删除:stringRedisTemplate.opsForSet().remove(key,id.toString());}return Result.ok();
}
然后就是求交集:
@Override
public Result commonFollow(Long id) {Long userId = UserHolder.getUser().getId();String keyPreFix="follows:";Set<String> intersect = stringRedisTemplate.opsForSet().intersect(keyPreFix + id, keyPreFix + userId);List<Long> collect = intersect.stream().map(Long::valueOf).collect(Collectors.toList());if (collect.isEmpty()||collect==null){return Result.ok();}List<User> users = userService.listByIds(collect);List<UserDTO> userDTOS = BeanUtil.copyToList(users, UserDTO.class);return Result.ok(userDTOS);}
三:关注推送
1:feed流实现方案分析
拉模式是将信息先存储到发件箱中,每次有人读的时候,再从发件箱中拉取消息到自己的收件箱,但是这种的话如果关注的人比较多,拉取的消息会变得异常的多,就会产生性能上的影响;
推模式是直接将用户发的消息推送到关注用户的收件箱里;然后用户打开直接就能看到,但是如果是大v他的粉丝很多,并且有很多僵尸粉,如果全部都推过去就会浪费内存
推拉结合:对于大v,他的活跃粉丝我们采用推模式,因为经常看,读取的次数较多,如果是僵尸粉,我们采用拉模式,将消息放在发件箱里,只有读取的时候才会拉取;
对于普通用户,因为关注他的人并不多,所以我们可以采用推模式,直接将消息推送到粉丝的收件箱中,这样也不会浪费太多的内存;
2:基于推模式实现关注推送功能
我们要选择一个redis的数据结构来作为用户的收件箱,收件箱里保存博客的id,这个收件箱的要求就是可以根据时间进行排序,有两个结构满足条件,一个是list,早放进去的就在前面,还有就是zset,将时间戳当成score,然后根据score进行排序;
然而我们的feed流是实时推送不断更新的,不能使用传统的分页查询,例如在读取第一页的时候,从最新的开始page=1,size=5,那么就会从索引为0开始读到4,但是这个时候又接受了新的消息,这个时候索引都变了,如果再读取第二页,page=2,size=5,开始从索引为5的读取,而因为接受了新的消息,原来索引为4的变成了索引为5的,就会出现重复读取,索引传统的分页不能使用
我们可以使用滚动分页,原理就是,page页码变成了最后一个读取的消息,那么list有办法记录最后一个读取的消息吗,没有因为list只有索引,而feed使索引也会变化;但是zset可以记录最后一个读取的score,所以我们选择zset;
完成推送的代码:
@Override
@Transactional
public Result saveBlog(Blog blog) {// 获取登录用户UserDTO user = UserHolder.getUser();blog.setUserId(user.getId());//查询所有该用户的所有粉丝List<Follow> list = followService.query().eq("follow_user_id", user.getId().toString()).list();for (Follow follow : list) {Long userId = follow.getUserId();stringRedisTemplate.opsForZSet().add(RedisConstants.FEED_KEY+userId,blog.getId().toString(),System.currentTimeMillis());}// 保存探店笔记save(blog);return Result.ok();
}
3:滚动分页查询
因为我们要根据score进行排序,所以要传入查询score的值的范围,score值的最大值,在第一次查询就是当前时间戳,在后面的查询的过程中就是上一次查询的score中的最小值,score的最小值就是0,这个不用管,然后就是offset,offset是,从获取到的score范围内第几个开始查,第一次查询都是0,后面的查询,都是从上一次查询的score的最小值的个数。开始查,然后就是查询的个数,这个也是固定的;
定义一个类来封装返回的参数:
@Data
public class ScrollResult {//存储这一次查询的数据private List<?> list;//存储这一次查询的最小时间戳private Long minTime;//存储这一次查询最小时间戳的个数private Integer offset;
}
然后就是service的代码:
@Override@Transactionalpublic Result queryBlogOFFollow(Long lastId, Integer offset) {//获取用户id用于查找用户的收件箱Long userId = UserHolder.getUser().getId();String key=RedisConstants.FEED_KEY+userId;//查询用户的收件箱,按指定方式查询:按照score倒序,socre最小值是0,最大值是传入的lastId(上一次查询的最小值),offset(偏移量),count(查询个数)Set<ZSetOperations.TypedTuple<String>> typedTuples =stringRedisTemplate.opsForZSet().reverseRangeByScoreWithScores(key, 0, lastId, offset, 3);//判断是否为空if (typedTuples==null||typedTuples.isEmpty()){return Result.ok();}//先定义好变量用于接受数据返回给前端ArrayList<Long> ids = new ArrayList<>(typedTuples.size());long minTime=0;Integer os=1;//遍历收件箱,获取blog的id,和时间戳for (ZSetOperations.TypedTuple<String> typedTuple : typedTuples) {String id = typedTuple.getValue();ids.add(Long.valueOf(id));long time = typedTuple.getScore().longValue();//时间戳就是一直更新,os是最小的时间戳出现的次数,如果不是最小的时间戳就是置为0if (minTime==time){os++;}else {minTime=time;os=1;}}//将blog的集合转成字符串并用‘,’隔开,然后因为mp的in直接传入集合会乱序,我们自己写order by field(顺序,也就是获取的id字符串)String join = StrUtil.join(",", ids);List<Blog> blogs = query().in("id", ids).last("order by field(id," + join + ")").list();//然后因为每个博客都有点赞和,用户信息,我们需要查询出来然后赋值for (Blog blog : blogs) {User user = userService.getById(userId);String nickName = user.getNickName();String icon = user.getIcon();blog.setIcon(icon);blog.setName(nickName);Double member = stringRedisTemplate.opsForZSet().score(RedisConstants.BLOG_LIKED_KEY + blog.getId(), UserHolder.getUser().getId().toString());blog.setIsLike(member != null);}//最后封装返回对象ScrollResult scrollResult = new ScrollResult();scrollResult.setList(blogs);scrollResult.setOffset(offset);scrollResult.setMinTime(minTime);return Result.ok(scrollResult);}
}
og.getId(), UserHolder.getUser().getId().toString());blog.setIsLike(member != null);}//最后封装返回对象ScrollResult scrollResult = new ScrollResult();scrollResult.setList(blogs);scrollResult.setOffset(offset);scrollResult.setMinTime(minTime);return Result.ok(scrollResult);}
}