Redis实践之好友关注
1.关注与取关
需求:基于该表数据结构,实现两个接口:
- 关注和取关接口
- 判断是否关注的接口
关注是User之间的关系,是博主与粉丝的关系,数据库中有一张tb_follow表来标示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public Result follow(Long followUserId, Boolean isFollow) { Long userId = UserHolder.getUser().getId(); if (BooleanUtil.isTrue(isFollow)) { Follow follow = new Follow(); follow.setFollowUserId(followUserId); follow.setUserId(userId); save(follow); } else { remove(new QueryWrapper<Follow>().eq("user_id", userId).eq("follow_user_id", followUserId)); } return Result.ok(); }
public Result isFollow(Long followUserId) { Long userId = UserHolder.getUser().getId(); Integer count = query().eq("user_id", userId).eq("follow_user_id", followUserId).count(); return Result.ok(count > 0); }
|
2.共同关注
先来完成查看用户主页的两个接口,这跟我们的共同关注功能无关,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| @GetMapping("/{id}") public Result queryUserById(@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); }
@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); }
|
需求:利用Redis中恰当的数据结构,实现共同关注功能。在博主个人页面展示出当前用户与博主的共同好友。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| public Result follow(Long followUserId, Boolean isFollow) { Long userId = UserHolder.getUser().getId(); if (BooleanUtil.isTrue(isFollow)) { Follow follow = new Follow(); follow.setFollowUserId(followUserId); follow.setUserId(userId); boolean isSuccess = save(follow); if (isSuccess) { stringRedisTemplate.opsForSet().add("follows:" + userId, followUserId.toString()); } } else { boolean isSuccess = remove(new QueryWrapper<Follow>().eq("user_id", userId).eq("follow_user_id", followUserId)); if (isSuccess) { stringRedisTemplate.opsForSet().remove("follows:" + userId, followUserId.toString()); } } return Result.ok(); }
public Result followCommons(Long followUserId) { Long userId = UserHolder.getUser().getId(); Set<String> commons = stringRedisTemplate.opsForSet().intersect("follows:" + userId, "follows:" + followUserId); if (commons == null || commons.isEmpty()) return Result.ok(Collections.emptyList()); List<Long> ids = commons.stream().map(Long::valueOf).collect(Collectors.toList()); List<UserDTO> userDTOS = userService.listByIds(ids).stream().map(user -> BeanUtil.copyProperties(user, UserDTO.class)).collect(Collectors.toList()); return Result.ok(userDTOS); }
|
3.关注推送
关注推送也叫做Feed流,直译为投喂。为用户持续的提供“沉浸式”的体验,通过无限下拉刷新获取新的信息。
Feed流产品有两种常见模式:
- Timeline:不做内容筛选,简单的按照内容发布时间排序,常用于好友或关注,例如朋友圈
- 优点:信息全面,不会有缺失,并且实现也相对简单
- 缺点:信息噪音较多,用户不一定感兴趣,内容获取效率低
- 智能排序:利用智能算法屏蔽掉违规的、用户不感兴趣的内容,推送用户感兴趣信息来吸引用户
- 优点:投喂用户感兴趣信息,用户粘度很高,容易沉迷
- 缺点:如果算法不精准,可能起到反作用
本例中的个人页面,是基于关注的好友来做Feed流,因此采用Timeline的模式。该模式的实现方案:
基于推模式实现关注推送功能:
- 修改新增探店笔记的业务,在保存blog到数据库的同时,推送到粉丝的收件箱
- 收件箱满足可以根据时间戳排序,必须用Redis的数据结构实现
- 查询收件箱数据时,可以实现分页查询
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public Result saveBlog(Blog blog) { UserDTO user = UserHolder.getUser(); blog.setUserId(user.getId()); boolean isSuccess = save(blog); if (!isSuccess) { return Result.fail("新增笔记失败!"); } List<Follow> follows = followService.query().eq("follow_user_id", user.getId()).list(); for (Follow follow : follows) { Long userId = follow.getUserId(); stringRedisTemplate.opsForZSet().add("feed:" + userId, blog.getId().toString(), System.currentTimeMillis()); } return Result.ok(blog.getId()); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| public Result queryBlogOfFollow(Long max, Integer offset) { UserDTO user = UserHolder.getUser(); Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet().reverseRangeByScoreWithScores("feed:" + user.getId(), 0, max, offset, 2); if (typedTuples == null || typedTuples.isEmpty()) { return Result.ok(); } List<Long> ids = new ArrayList<>(typedTuples.size()); long minTime = 0; int count = 1; for (ZSetOperations.TypedTuple<String> typedTuple : typedTuples) { String blogId = typedTuple.getValue(); long time = typedTuple.getScore().longValue(); if (time == minTime) { count++; } else { minTime = time; count = 1; } } String idStr = StrUtil.join(",", ids); List<Blog> blogs = query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list(); for (Blog blog : blogs) { Long userId = blog.getUserId(); User u = userService.getById(userId); blog.setName(u.getNickName()); blog.setIcon(u.getIcon()); blog.setIsLike(isMember(blog, userId)); } ScrollResult scrollResult = new ScrollResult(); scrollResult.setList(blogs); scrollResult.setMinTime(minTime); scrollResult.setOffset(count); return Result.ok(scrollResult); }
|