Redis从入门到入土 --- 共同关注功能后端实现

0 阅读5分钟

在做黑马点评项目时,共同关注 是一个很典型的业务功能。

需求不复杂:

用户 A 访问用户 B 的主页时,可以查看自己与用户 B 共同关注了哪些人。

看起来简单,但背后涉及了几个值得深挖的技术点:

  • 关系型数据的建模方式
  • Redis Set 的天然优势
  • 数据库与缓存的双写策略
  • 接口的合理设计

本文结合项目代码,系统梳理一下这个功能的后端实现思路,也方便自己后续复习。


一、需求分析

共同关注本质上就是一个集合求交集的问题。

举例:

  • 用户 A 关注了:[1、2、3、4]
  • 用户 B 关注了:[3、4、5、6]

那么 A 和 B 的共同关注就是:[3、4]

所以这个功能的核心分两步:

  1. 存储:保存"谁关注了谁"的关系
  2. 查询:高效地求出两个用户关注集合的交集

二、为什么用 Redis Set,而不只用数据库?

如果纯用数据库,也能实现,比如 SQL 自连接,或者两次查询后在内存里取交集。

但在高并发场景下,这类操作会给数据库带来不小的压力。Redis Set 正好能解决这个问题。

Redis Set 有两个天然的特性特别符合这个需求:

元素唯一性

关注关系本来就不能重复,Set 的唯一性约束和业务语义完全匹配。

原生支持集合运算

Redis 提供了 SINTERSTORE / SINTER 等命令,可以直接在 Redis 层面完成交集运算,效率极高。

所以整体方案是数据库 + Redis 双写

层级职责
数据库持久化存储,保证数据不丢失
Redis Set加速交集查询,提升响应性能

一句话总结:数据库是准的,Redis 是快的。


三、Redis 数据结构设计

每个用户维护一个 Set,存储其关注的所有用户 id:

Key:   follows:{userId}
Value: Set{ followUserId1, followUserId2, ... }

例如用户 100 关注了 200、201、202:

follows:100 → { "200", "201", "202" }

代码中常量定义:

public static final String FOLLOW_KEY = "follows:";

拼接完整 Key:

String key = FOLLOW_KEY + userId;


四、接口设计

共同关注功能对外提供 3 个接口:

1. 关注 / 取关

PUT /follow/{id}/{isFollow}

参数说明
id目标用户 id
isFollowtrue = 关注,false = 取关

2. 是否关注

GET /follow/or/not/{id}

用于前端渲染"已关注 / 未关注"的按钮状态。

3. 共同关注列表

GET /follow/common/{id}

id 为目标用户 id,表示查询当前登录用户与目标用户的共同关注列表。


五、数据库实体设计

tb_follow 表对应的实体类:

@Data
@TableName("tb_follow")
public class Follow {

    @TableId(type = IdType.AUTO)
    private Long id;

    /** 当前登录用户 id */
    private Long userId;

    /** 被关注用户 id */
    private Long followUserId;

    private LocalDateTime createTime;
}

六、关注 / 取关实现

整体思路

关注:写入数据库 → 写入 Redis Set

取关:删除数据库记录 → 从 Redis Set 移除

为什么先操作数据库,再操作 Redis?

数据库是最终数据来源,Redis 只是加速层。遵循先写库、成功后再更新缓存的原则,可以避免 Redis 中存着脏数据而数据库写失败的情况。

核心代码

public Result follow(Long followUserId, Boolean isFollow) {
    // 1.获取登录用户
    Long userId = UserHolder.getUser().getId();
    String key = FOLLOW_KEY + userId;
    // 2.判断到底是关注还是取关
    if (isFollow) {
        // 3.关注,新增数据
        Follow follow = new Follow();
        follow.setUserId(userId);
        follow.setFollowUserId(followUserId);
        boolean isSuccess = save(follow);
        if (isSuccess) {
            // 把关注的用户id放入Redis的set集合sadd userId followUserId
            stringRedisTemplate.opsForSet().add(key, followUserId.toString());
        }
    } else {
        // 4.取关,删除 delete from tb_follow where userId = ? and follow_user_id = ?
        boolean isSuccess = remove(new QueryWrapper<Follow>()
                .eq("user_id", userId)
                .eq("follow_user_id", followUserId));
        if (isSuccess) {
            // 把关注的用户ids从Redis的set集合移除
            stringRedisTemplate.opsForSet().remove(key, followUserId.toString());
        }
    }
    return Result.ok();
}

七、是否关注实现

这个接口逻辑简单,直接查数据库记录数即可:

public Result isFollow(Long followUserId) {
    // 1.获取登录用户
    UserDTO user = UserHolder.getUser();
    // 2.查询是否关注 select count(*) from tb_follow where userId = ? and follow_user_id = ?
    Integer count = query().eq("user_id", user.getId()).eq("follow_user_id", followUserId).count();
    // 3.判断
    return Result.ok(count > 0);
}

八、共同关注实现

这是本模块最核心的部分,完整流程如下:

image.png

核心代码

public Result followCommons(Long id) {
    // 1.获取当前用户
    Long userId = UserHolder.getUser().getId();
    String key = FOLLOW_KEY + userId;
    // 求交集
    String key2 = FOLLOW_KEY + id;
    Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key, key2);
    if (intersect == null || intersect.isEmpty()) {
        // 无交集
        return Result.ok(Collections.emptyList());
    }
    // 3.解析id集合
    List<Long> ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList());
    // 4.查询用户
    List<UserDTO> users = userService.listByIds(ids)
            .stream()
            .map(user -> BeanUtil.copyProperties(user, UserDTO.class))
            .collect(Collectors.toList());
    return Result.ok(users);
}

注意listByIds 底层走的是 WHERE id IN (...) 查询,返回顺序不保证与传入 id 一致。若需要按特定顺序展示共同关注列表,需要在业务层手动排序。


九、Controller 层

/**
 * 关注 / 取关
 */
@PutMapping("/{id}/{isFollow}")
public Result follow(@PathVariable("id") Long followUserId, @PathVariable("isFollow") Boolean isFollow) {
    return followService.follow(followUserId, isFollow);
}

/**
 * 是否关注
 */
@GetMapping("/or/not/{id}")
public Result follow(@PathVariable("id") Long followUserId) {
    return followService.isFollow(followUserId);
}

/**
 * 共同关注
 */
@GetMapping("/common/{id}")
public Result followCommons(@PathVariable("id") Long id) {
    return followService.followCommons(id);
}

十、完整执行流程梳理

关注流程

image.png

取关流程

image.png

共同关注流程

image.png


十一、小结

功能点实现方式
关注/取关数据库 + Redis Set 双写
是否关注查数据库 count
共同关注Redis SINTER 求交集,再查用户详情

Redis Set 在这个场景下是一个非常自然的选择:唯一性天然契合关注关系,SINTER 命令(opsForSet().intersect())让求共同关注变得极其简洁高效。理解了这个模块,对 Redis 在实际业务中的应用会有更直观的感受。