Redis实战(黑马点评--好友关注)
关注和取消关注
当我们进入到笔记详情页面时,会发送一个请求,判断当前登录用户是否关注了笔记博主
1 2 | 请求网址: http: //localhost:8080/api/follow/or/not/2 请求方法: GET |
当我们点击关注按钮时,会发送一个请求,实现关注/取关
1 2 | 请求网址: http: //localhost:8080/api/follow/2/true 请求方法: PUT |
-
关注是User之间的关系,是博主与粉丝的关系,数据库中有一张tb_follow表来标示
Field | Type | Collation | Null | Key | Default | Extra | Comment |
---|---|---|---|---|---|---|---|
id | bigint | (NULL) | NO | PRI | (NULL) | auto_increment | 主键 |
user_id | bigint unsigned | (NULL) | NO | (NULL) | 用户id | ||
follow_user_id | bigint unsigned | (NULL) | NO | (NULL) | 关联的用户id | ||
create_time | timestamp | (NULL) | NO | CURRENT_TIMESTAMP | DEFAULT_GENERATED | 创建时间 |
对应的实体类如下
1 @Data 2 @EqualsAndHashCode(callSuper = false) 3 @Accessors(chain = true) 4 @TableName("tb_follow") 5 public class Follow implements Serializable { 6 7 private static final long serialVersionUID = 1L; 8 9 /** 10 * 主键 11 */ 12 @TableId(value = "id", type = IdType.AUTO) 13 private Long id; 14 15 /** 16 * 用户id 17 */ 18 private Long userId; 19 20 /** 21 * 关联的用户id 22 */ 23 private Long followUserId; 24 25 /** 26 * 创建时间 27 */ 28 private LocalDateTime createTime; 29 }
Controller层中编写对应的两个方法
@RestController @RequestMapping("/follow") public class FollowController { @Resource private IFollowService followService; //判断当前用户是否关注了该博主 @GetMapping("/or/not/{id}") public Result isFollow(@PathVariable("id") Long followUserId) { return followService.isFollow(followUserId); } //实现取关/关注 @PutMapping("/{id}/{isFollow}") public Result follow(@PathVariable("id") Long followUserId, @PathVariable("isFollow") Boolean isFellow) { return followService.follow(followUserId,isFellow); } }
具体的业务逻辑
@Service public class FollowServiceImpl extends ServiceImpl<FollowMapper, Follow> implements IFollowService { @Override public Result isFollow(Long followUserId) { //获取当前登录的userId Long userId = UserHolder.getUser().getId(); LambdaQueryWrapper<Follow> queryWrapper = new LambdaQueryWrapper<>(); //查询当前用户是否关注了该笔记的博主 queryWrapper.eq(Follow::getUserId, userId).eq(Follow::getFollowUserId, followUserId); //只查询一个count就行了 int count = this.count(queryWrapper); return Result.ok(count > 0); } @Override public Result follow(Long followUserId, Boolean isFellow) { //获取当前用户id Long userId = UserHolder.getUser().getId(); //判断是否关注 if (isFellow) { //关注,则将信息保存到数据库 Follow follow = new Follow(); follow.setUserId(userId); follow.setFollowUserId(followUserId); save(follow); } else { //取关,则将数据从数据库中移除 LambdaQueryWrapper<Follow> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(Follow::getUserId, userId).eq(Follow::getFollowUserId, followUserId); remove(queryWrapper); } return Result.ok(); } }
共同关注
点击用户头像,进入到用户详情页,可以查看用户发布的笔记,和共同关注列表
查看发送的请求
-
检测NetWork选项卡,查看发送的请求
-
查询用户信息
请求网址: http://localhost:8080/api/user/2
请求方法: GET -
查看共同关注
请求网址: http://localhost:8080/api/follow/common/undefined
请求方法: GET
-
编写查询用户信息
方法
@GetMapping("/{id}") public Result queryById(@PathVariable("id") Long userId) { // 查询详情 User user = userService.getById(userId); if (user == null) { // 没有详情,应该是第一次查看详情 return Result.ok(); } UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class); // 返回 return Result.ok(userDTO); }
-
重启服务器,现在可以看到用户信息,但是不能看到用户发布的笔记信息,查看NetWork检测的请求,我们还需要完成这个需求
请求网址: http://localhost:8080/api/blog/of/user?&id=2¤t=1
请求方法: GET
编写查询用户笔记
方法
@GetMapping("/of/user") public Result queryBlogByUserId(@RequestParam(value = "current", defaultValue = "1") Integer current, @RequestParam("id") Long id) { LambdaQueryWrapper<Blog> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(Blog::getUserId, id); Page<Blog> pageInfo = new Page<>(current, SystemConstants.MAX_PAGE_SIZE); blogService.page(pageInfo, queryWrapper); List<Blog> records = pageInfo.getRecords(); return Result.ok(records); } //下面这是老师的代码,个人感觉我的可读性更高[doge] // BlogController 根据id查询博主的探店笔记 @GetMapping("/of/user") public Result queryBlogByUserId( @RequestParam(value = "current", defaultValue = "1") Integer current, @RequestParam("id") Long id) { // 根据用户查询 Page<Blog> page = blogService.query() .eq("user_id", id).page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE)); // 获取当前页数据 List<Blog> records = page.getRecords(); return Result.ok(records); }
-
接下来我们来看看怎么实现共同关注
- 实现方式当然是我们之前学过的set集合,在set集合中,有交集并集补集的api,可以把二者关注的人放入到set集合中,然后通过api查询两个set集合的交集
-
那我们就得先修改我们之前的关注逻辑,在关注博主的同时,需要将数据放到set集合中,方便后期我们实现共同关注,当取消关注时,也需要将数据从set集合中删除
@Resource private StringRedisTemplate stringRedisTemplate; @Override public Result follow(Long followUserId, Boolean isFellow) { //获取当前用户id Long userId = UserHolder.getUser().getId(); String key = "follows:" + userId; //判断是否关注 if (isFellow) { //关注,则将信息保存到数据库 Follow follow = new Follow(); follow.setUserId(userId); follow.setFollowUserId(followUserId); //如果保存成功 boolean success = save(follow); //则将数据也写入Redis if (success) { stringRedisTemplate.opsForSet().add(key, followUserId.toString()); } } else { //取关,则将数据从数据库中移除 LambdaQueryWrapper<Follow> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(Follow::getUserId, userId).eq(Follow::getFollowUserId, followUserId); //如果取关成功 boolean success = remove(queryWrapper); //则将数据也从Redis中移除 if (success){ stringRedisTemplate.opsForSet().remove(key,followUserId.toString()); } } return Result.ok(); }
实现共同关注代码
@GetMapping("/common/{id}") public Result followCommons(@PathVariable Long id){ return followService.followCommons(id); }
@Override public Result followCommons(Long id) { //获取当前用户id Long userId = UserHolder.getUser().getId(); String key1 = "follows:" + id; String key2 = "follows:" + userId; //对当前用户和博主用户的关注列表取交集 Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key1, key2); if (intersect == null || intersect.isEmpty()) { //无交集就返回个空集合 return Result.ok(Collections.emptyList()); } //将结果转为list List<Long> ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList()); //之后根据ids去查询共同关注的用户,封装成UserDto再返回 List<UserDTO> userDTOS = userService.listByIds(ids).stream().map(user -> BeanUtil.copyProperties(user, UserDTO.class)).collect(Collectors.toList()); return Result.ok(userDTOS); }
Feed流实现方案
- 当我们关注了用户之后,这个用户发布了动态,那我们应该把这些数据推送给用户,这个需求,我们又称其为Feed流,关注推送也叫作Feed流,直译为投喂,为用户提供沉浸式体验,通过无限下拉刷新获取新的信息,
- 对于传统的模式内容检索:用户需要主动通过搜索引擎或者是其他方式去查找想看的内容
- 对于新型Feed流的效果:系统分析用户到底想看什么,然后直接把内容推送给用户,从而使用户能更加节约时间,不用去主动搜素
- Feed流的实现有两种模式
- Timeline:不做内容筛选,简单的按照内容发布时间排序,常用于好友或关注(B站关注的up,朋友圈等)
- 优点:信息全面,不会有缺失,并且实现也相对简单
- 缺点:信息噪音较多,用户不一定感兴趣,内容获取效率低
- 智能排序:利用智能算法屏蔽掉违规的、用户不感兴趣的内容,推送用户感兴趣的信息来吸引用户
- 优点:投喂用户感兴趣的信息,用户粘度很高,容易沉迷
- 缺点:如果算法不精准,可能会起到反作用(给你推的你都不爱看)
- Timeline:不做内容筛选,简单的按照内容发布时间排序,常用于好友或关注(B站关注的up,朋友圈等)
- 那我们这里针对好友的操作,采用的是Timeline方式,只需要拿到我们关注用户的信息,然后按照时间排序即可
- 采用Timeline模式,有三种具体的实现方案
- 拉模式
- 推模式
- 推拉结合
拉模式
:也叫读扩散推模式
:也叫写扩散推拉结合
:页脚读写混合,兼具推和拉两种模式的优点
推送到粉丝收件箱
- 需求:
- 修改新增探店笔记的业务,在保存blog到数据库的同时,推送到粉丝的收件箱
- 收件箱满足可以根据时间戳排序,必须使用Redis的数据结构实现
- 查询收件箱数据时,课实现分页查询
- Feed流中的数据会不断更新,所以数据的角标也会不断变化,所以我们不能使用传统的分页模式
-
假设在t1时刻,我们取读取第一页,此时page = 1,size = 5,那么我们拿到的就是
10~6
这几条记录,假设t2时刻有发布了一条新纪录,那么在t3时刻,我们来读取第二页,此时page = 2,size = 5,那么此时读取的数据是从6开始的,读到的是6~2
,那么我们就读到了重复的数据,所以我们要使用Feed流的分页,不能使用传统的分页 -
Feed流的滚动分页
- 核心思路:我们保存完探店笔记后,获取当前用户的粉丝列表,然后将数据推送给粉丝
- 那现在我们就需要修改保存笔记的方法
@Override public Result saveBlog(Blog blog) { // 获取登录用户 UserDTO user = UserHolder.getUser(); blog.setUserId(user.getId()); // 保存探店博文 save(blog); // 条件构造器 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) { Long userId = follow.getUserId(); String key = FEED_KEY + userId; //推送数据 stringRedisTemplate.opsForZSet().add(key, blog.getId().toString(), System.currentTimeMillis()); } // 返回id return Result.ok(blog.getId()); }
实现分页查询收件箱
- 需求:在个人主页的
关注栏
中,查询并展示推送的Blog信息 - 具体步骤如下
- 每次查询完成之后,我们要分析出查询出的最小时间戳,这个值会作为下一次的查询条件
- 我们需要找到与上一次查询相同的查询个数,并作为偏移量,下次查询的时候,跳过这些查询过的数据,拿到我们需要的数据(例如时间戳8 6 6 5 5 4,我们每次查询3个,第一次是8 6 6,此时最小时间戳是6,如果不设置偏移量,会从第一个6之后开始查询,那么查询到的就是6 5 5,而不是5 5 4,如果这里说的不清楚,那就看后续的代码)
- 综上:我们的请求参数中需要携带lastId和offset,即上一次查询时的最小时间戳和偏移量,这两个参数
- 编写一个通用的实体类,不一定只对blog进行分页查询,这里用泛型做一个通用的分页查询,list是封装返回的结果,minTime是记录的最小时间戳,offset是记录偏移量
@Data public class ScrollResult { private List<?> list; private Long minTime; private Integer offset; }
在BlogController中创建对应的方法,具体实现去ServiceImpl中完成
@GetMapping("/of/follow") 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. 获取当前用户 Long userId = UserHolder.getUser().getId(); //2. 查询该用户收件箱(之前我们存的key是固定前缀 + 粉丝id),所以根据当前用户id就可以查询是否有关注的人发了笔记 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. 解析数据,blogId、minTime(时间戳)、offset,这里指定创建的list大小,可以略微提高效率,因为我们知道这个list就得是这么大 ArrayList<Long> ids = new ArrayList<>(typeTuples.size()); long minTime = 0; int os = 1; for (ZSetOperations.TypedTuple<String> typeTuple : typeTuples) { //4.1 获取id String id = typeTuple.getValue(); ids.add(Long.valueOf(id)); //4.2 获取score(时间戳) long time = typeTuple.getScore().longValue(); if (time == minTime){ os++; }else { minTime = time; os = 1; } } //解决SQL的in不能排序问题,手动指定排序为传入的ids String idsStr = StrUtil.join(","); //5. 根据id查询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); }
【推荐】博客园携手 AI 驱动开发工具商 Chat2DB 推出联合终身会员
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 2024年终总结 : 迷茫, 尝试突破, 内耗, 释怀
· 开源商业化 Sealos 如何做到月入 160万
· 《花100块做个摸鱼小网站! 》番外篇—小网站竟然让我赚到钱了
· 2025你好
· Coravel:一个可轻松实现任务调度、队列、邮件发送的开源项目