Redis实践之好友关注

Redis实践之好友关注

1.关注与取关

image-20240112113504573

需求:基于该表数据结构,实现两个接口:

  1. 关注和取关接口
  2. 判断是否关注的接口

关注是User之间的关系,是博主与粉丝的关系,数据库中有一张tb_follow表来标示:

image-20240112113633589

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) {
// 获取当前登录用户ID
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) {
// 获取当前登录用户ID
Long userId = UserHolder.getUser().getId();
// 查询是否关注
Integer count = query().eq("user_id", userId).eq("follow_user_id", followUserId).count();
return Result.ok(count > 0);
}

2.共同关注

image-20240112113915316

先来完成查看用户主页的两个接口,这跟我们的共同关注功能无关,代码如下:

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中恰当的数据结构,实现共同关注功能。在博主个人页面展示出当前用户与博主的共同好友。

image-20240112114259205

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) {
// 获取当前登录用户ID
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) {
// 获取当前登录用户ID
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());
// 解析用户ID
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流,直译为投喂。为用户持续的提供“沉浸式”的体验,通过无限下拉刷新获取新的信息。

image-20240112114700368

Feed流产品有两种常见模式:

  • Timeline:不做内容筛选,简单的按照内容发布时间排序,常用于好友或关注,例如朋友圈
    • 优点:信息全面,不会有缺失,并且实现也相对简单
    • 缺点:信息噪音较多,用户不一定感兴趣,内容获取效率低
  • 智能排序:利用智能算法屏蔽掉违规的、用户不感兴趣的内容,推送用户感兴趣信息来吸引用户
    • 优点:投喂用户感兴趣信息,用户粘度很高,容易沉迷
    • 缺点:如果算法不精准,可能起到反作用

本例中的个人页面,是基于关注的好友来做Feed流,因此采用Timeline的模式。该模式的实现方案:

  • 拉模式
  • 推模式
  • 推拉结合

image-20240112115218029

image-20240112115416429

image-20240112115819126

image-20240112115833203

基于推模式实现关注推送功能:

  • 修改新增探店笔记的业务,在保存blog到数据库的同时,推送到粉丝的收件箱
  • 收件箱满足可以根据时间戳排序,必须用Redis的数据结构实现
  • 查询收件箱数据时,可以实现分页查询

image-20240112120113299

image-20240112120130013

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();
// 推送笔记ID给所有粉丝
for (Follow follow : follows) {
// 获取粉丝ID
Long userId = follow.getUserId();
// 推送到粉丝收件箱
stringRedisTemplate.opsForZSet().add("feed:" + userId, blog.getId().toString(), System.currentTimeMillis());
}
// 返回id
return Result.ok(blog.getId());
}

image-20240112120527922

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) {
// 获取笔记ID
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);
}