黑马点评-Redis Set-实现关注、取关和共同关注

Redis Set 实现关注、取关和共同关注:黑马点评第 9 章前半部分学习笔记

本文整理自黑马点评 Redis 实战篇第 9 章前半部分。第 9 章的主题是「好友关注」,前半部分主要解决两个问题:用户如何关注/取关另一个用户,以及如何用 Redis Set 快速查询共同关注。

1. 这一篇解决什么问题

黑马点评里有一个很常见的社交功能:

text 复制代码
我可以关注一个博主;
我也可以取消关注;
进入某个博主主页时,可以看到我和他都关注了哪些人。

这里其实有两层问题:

text 复制代码
第一层:关注关系怎么存?
第二层:共同关注怎么快速查?

第 9.1 小节解决第一层,用 MySQL 的 tb_follow 表保存关注关系。

第 9.2 小节解决第二层,用 Redis Set 保存每个用户关注的人,然后通过交集查询共同关注。


2. 关注关系本质上是什么

关注关系不是用户和博客的关系,而是用户和用户的关系。

比如:

text 复制代码
用户 5 关注了用户 10
用户 6 关注了用户 10
用户 5 关注了用户 20

这说明:

text 复制代码
用户 10 有两个粉丝:5 和 6
用户 5 关注了两个人:10 和 20

项目中用 tb_follow 表表示这种关系,对应实体类是:

java 复制代码
@TableName("tb_follow")
public class Follow implements Serializable {
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    private Long userId;
    private Long followUserId;
    private LocalDateTime createTime;
}

最重要的是这两个字段:

text 复制代码
userId        发起关注的人,也就是粉丝
followUserId  被关注的人,也就是博主

如果数据库里有一条记录:

text 复制代码
user_id = 5
follow_user_id = 10

意思是:

text 复制代码
用户 5 关注了用户 10。

这个方向非常重要,后面 Feed 流推送时也会依赖它。


3. 关注和取关接口

Controller 入口:

java 复制代码
@PutMapping("/{id}/{isFollow}")
public Result follow(@PathVariable("id") Long followUserId,
                     @PathVariable("isFollow") Boolean isFollow) {
    return followService.follow(followUserId, isFollow);
}

请求示例:

text 复制代码
PUT /follow/10/true

表示当前登录用户关注用户 10。

text 复制代码
PUT /follow/10/false

表示当前登录用户取消关注用户 10。

这里没有让前端传当前用户 id,因为当前用户应该由服务端从登录上下文中获取:

java 复制代码
Long userId = UserHolder.getUser().getId();

这样可以避免用户伪造请求替别人关注或取关。


4. 关注和取关的核心代码

Service 实现:

java 复制代码
@Override
public Result follow(Long followUserId, Boolean isFollow) {
    Long userId = UserHolder.getUser().getId();
    String key = "follows:" + userId;

    if (isFollow) {
        Follow follow = new Follow();
        follow.setUserId(userId);
        follow.setFollowUserId(followUserId);
        boolean isSuccess = save(follow);
        if(isSuccess){
            stringRedisTemplate.opsForSet().add(key, followUserId.toString());
        }
    } else {
        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();
}

先看 MySQL 部分。

关注时:

java 复制代码
Follow follow = new Follow();
follow.setUserId(userId);
follow.setFollowUserId(followUserId);
save(follow);

等价于:

sql 复制代码
INSERT INTO tb_follow(user_id, follow_user_id)
VALUES(当前登录用户id, 被关注用户id);

取关时:

java 复制代码
remove(new QueryWrapper<Follow>()
        .eq("user_id", userId)
        .eq("follow_user_id", followUserId));

等价于:

sql 复制代码
DELETE FROM tb_follow
WHERE user_id = 当前登录用户id
  AND follow_user_id = 被关注用户id;

所以关注功能本质上很简单:

text 复制代码
关注:新增一条关系记录
取关:删除一条关系记录

5. 判断是否关注

进入博主主页时,前端需要知道当前用户是否已经关注了这个博主,从而决定按钮显示「关注」还是「已关注」。

Controller:

java 复制代码
@GetMapping("/or/not/{id}")
public Result isFollow(@PathVariable("id") Long followUserId) {
    return followService.isFollow(followUserId);
}

Service:

java 复制代码
@Override
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);
}

它对应的 SQL 是:

sql 复制代码
SELECT COUNT(*)
FROM tb_follow
WHERE user_id = 当前登录用户id
  AND follow_user_id = 被查看的用户id;

如果数量大于 0,说明已经关注。


6. 为什么 9.2 要引入 Redis Set

9.1 只用 MySQL 就可以完成关注、取关、判断是否关注。

但 9.2 要做「共同关注」。

假设当前用户是 5:

text 复制代码
用户 5 关注了:10, 20, 30

现在打开用户 8 的主页:

text 复制代码
用户 8 关注了:20, 30, 40

那么共同关注就是:

text 复制代码
20, 30

这本质上是集合交集:

text 复制代码
{10, 20, 30} ∩ {20, 30, 40} = {20, 30}

Redis Set 天然适合这个场景,因为它支持交集运算。


7. Redis 中怎么保存关注集合

每个用户维护一个 Set:

text 复制代码
key: follows:{userId}
value: 该用户关注的所有用户 id

比如:

text 复制代码
follows:5 = {10, 20, 30}
follows:8 = {20, 30, 40}

关注成功时:

java 复制代码
stringRedisTemplate.opsForSet().add(key, followUserId.toString());

对应 Redis 命令:

redis 复制代码
SADD follows:5 10

取关成功时:

java 复制代码
stringRedisTemplate.opsForSet().remove(key, followUserId.toString());

对应 Redis 命令:

redis 复制代码
SREM follows:5 10

这里 MySQL 是真实数据源,Redis Set 是为了共同关注查询做的加速结构。


8. 共同关注接口

Controller:

java 复制代码
@GetMapping("/common/{id}")
public Result followCommons(@PathVariable("id") Long id){
    return followService.followCommons(id);
}

请求示例:

text 复制代码
GET /follow/common/8

意思是:

text 复制代码
查询当前登录用户和用户 8 的共同关注。

Service:

java 复制代码
@Override
public Result followCommons(Long id) {
    Long userId = UserHolder.getUser().getId();
    String key = "follows:" + userId;
    String key2 = "follows:" + id;
    Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key, key2);
    if (intersect == null || intersect.isEmpty()) {
        return Result.ok(Collections.emptyList());
    }
    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);
}

核心是这一行:

java 复制代码
Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key, key2);

它对应 Redis 命令:

redis 复制代码
SINTER follows:5 follows:8

得到共同关注的用户 id 后,再回 MySQL 查询用户信息,并转成 UserDTO 返回给前端。


9. 数据链路总结

关注链路

text 复制代码
用户点击关注
  -> PUT /follow/{id}/true
  -> 获取当前登录用户 userId
  -> tb_follow 新增关系
  -> Redis: SADD follows:{userId} {followUserId}

取关链路

text 复制代码
用户点击取消关注
  -> PUT /follow/{id}/false
  -> 获取当前登录用户 userId
  -> tb_follow 删除关系
  -> Redis: SREM follows:{userId} {followUserId}

共同关注链路

text 复制代码
进入博主主页
  -> GET /follow/common/{id}
  -> 取 follows:{当前用户id}
  -> 取 follows:{博主id}
  -> Redis 求交集
  -> 根据交集 id 查用户信息
  -> 返回共同关注用户列表

10. 容易踩的坑

1. userId 和 followUserId 容易搞反

text 复制代码
userId        当前登录用户,发起关注的人
followUserId  被关注的人,博主

2. /follow/or/not/{id} 不是取消关注接口

它实际是判断是否关注接口。

3. Redis Set 不是真实数据源

真实关注关系仍然在 MySQL 的 tb_follow 表。Redis Set 是为了共同关注快速求交集。

4. 共同关注查的是「两个人关注的人」的交集

不是粉丝交集,也不是谁关注了我。


11. 面试怎么说

如果面试官问:共同关注怎么实现?

可以回答:

关注关系本身保存在 MySQL 的 tb_follow 表中,字段 user_id 表示发起关注的人,follow_user_id 表示被关注的人。为了快速查询共同关注,我在用户关注或取关时同步维护 Redis Set,key 是 follows:{userId},value 是该用户关注的用户 id。查询共同关注时,对当前用户和目标用户的两个 Set 做交集 SINTER,得到共同关注的用户 id,再回数据库查询用户信息返回。


12. 总结

第 9 章前半部分可以浓缩成一句话:

MySQL 负责保存真实关注关系,Redis Set 负责把每个用户关注的人组织成集合,从而用交集快速实现共同关注。

相关推荐
吴声子夜歌1 小时前
SQL进阶——HAVING子句
数据库·sql
无小道2 小时前
Redis——哨兵
数据库·redis·缓存·哨兵
AOwhisky2 小时前
Kubernetes(K8s)学习笔记(第十四期):集群存储与有状态应用(下篇):StatefulSet 有状态应用管理
redis·笔记·mysql·云原生·kubernetes·云计算·k8s
爱奥尼欧2 小时前
轻量级可扩展日志框架-异步日志与系统集成
开发语言·数据库·c++·学习
爱奥尼欧2 小时前
轻量级可扩展日志框架-日志落地与日志器模块实现
jvm·数据库·c++
ycydynq3 小时前
Django利用中间间 判断页面是否登录,未登录则返回登录页
数据库·django·sqlite
承渊政道3 小时前
【MySQL数据库学习】(MySQL访问、连接池原理与简易网站数据流动)
数据库·学习·mysql·mysql访问·连接池原理
吴声子夜歌3 小时前
SQL进阶——EXISTS谓词
java·数据库·sql
wefg15 小时前
【MySQL】索引(索引底层原理/创建/查看/删除主键、普通、联合、前缀、全文索引)
数据库·mysql