Redis - 集合 Set 及代码实战

Set 类型

  1. 定义:类似 Java 中的 HashSet 类,key 是 set 的名字,value 是集合中的值
  2. 特点
    1. 无序
    2. 元素唯一
    3. 查找速度快
    4. 支持交集、并集、补集功能

常见命令

命令 功能
SADD key member ... 添加元素
SREM key member ... 删除元素
SCARD key 获取元素个数
SISMEMBER key member 判断一个元素是否存在于 set 中
SMEMBERS 获取 set 中所有元素
SINTER key1 key2 ... 求 key1 和 key2 集合的交集
SDIFF key1 key2 ... 求 key1 和 key2 集合的差集
SUNION key1 key2 .... 求 key1 和 key2 集合的并集

编码方式

  1. IntSet 编码
    1. 定义:IntSet 是一个有序的整数数组结构,相比哈希表占用更少的内存空间
    2. 使用条件:当集合中存储的所有数据都是整数,并且元素数量不超过配置项 set-max-intset-entries(默认值为512)
    3. 功能:满足使用条件时 Redis 自动使用 IntSet 编码,减少内存占用
  2. HT(Hash Table)编码
    1. 定义:key 存储集合的元素,value 统一设置为 null(因为 Set 只关心元素是否存在,不需要存储值)
    2. 使用条件:当不满足 IntSet 编码条件时,Redis 会使用哈希表来存储集合
    3. 功能:提供快速的查找性能,但需要消耗更多内存

示例

  1. 目标:用户关注与取关博主,查看用户与博主的共同关注
  2. 注意:此处代码实现涉及较多 SpringBoot 和 MybatisPlus 相关知识,已默认读者有一定基础

功能点

  1. 判断当前登录用户是否已经关注当前博主
  2. 当前用户关注 & 取关当前博主
  3. 查询当前用户与当前博主的共同关注

业务方案

Set 类型(Redis)

  1. 功能:记录当前用户关注的所有博主,并且可以查看共同关注(交集操作)

  2. 数据结构 :Set

    key value (set)
    follow:userId(prefix + 做出关注行为的用户 id) 被 key 用户关注的用户 id 集合

MySQL

  1. 功能

    1. 记录所有关注与被关注关系:创建一个关注表,每个条目对应一个关注关系
    2. 查询是否关注:select * from subscribe_table where user_id = userId and follow_user_id followUserId (查询结果不为空则已关注)
    3. 查询关注列表:select follow_user_id from subscribe_table where user_id = userId (查询结果是所有 userId 关注的博主的id)
  2. 数据结构

    字段名 功能
    id primary key (自增)
    user_id 做出关注行为的用户的 ID
    follow_user_id 被关注的用户的 ID
    create_time 创建时间

最终方案

  1. 利用 Redis Set 的快速查询某个用户是否已经关注另一个用户
  2. 利用 Redis Set 的交集操作快速实现共同关注功能⁠
  3. 利用 MySQL 的 follow 表完整记录并持久化所有关注与被关注的关系⁠⁠
  4. 使用 MySQL 存储关注关系的基础数据,并使用Redis Set来提升共同关注等高频查询场景的性能⁠

代码实现

  1. 配置文件

    1. 目标:自动移除非活跃用户的关注列表,下次访问时再通过 MySQL 重建缓存

    2. 方案:使用 LRU(Least Recently Used)缓存淘汰策略。当内存超出阈值时,自动淘汰最久未使用的数据

    3. 注意:需要为 follow 缓存设置独立的 key 前缀,并结合 maxmemory-policy 配置分区缓存策略,避免误删其他缓存数据

      maxmemory-policy allkeys-lru

  2. 实体类 Follow:

    复制代码
    @Data
    @TableName("follow")
    public class Follow {
        @TableId(type = IdType.AUTO)
        private Long id;
        private Long userId;
        private Long followUserId;
        @TableField(fill = FieldFill.INSERT)
        private LocalDateTime createTime;
    }
  3. Controller

    复制代码
    @RestController
    @RequestMapping("/follow")
    public class FollowController {
    
        @Resource
        private IFollowService followService;
    
        @GetMapping("/isFollow/{followUserId}")
        public Result isFollow(@PathVariable Long followUserId) {
            boolean isFollow = followService.isFollow(followUserId);
            return Result.ok(isFollow);
        }
    
        @PostMapping("/follow/{followUserId}")
        public Result follow(@PathVariable Long followUserId) {
            boolean followExecuted = followService.follow(followUserId, isFollow);
            return Result.ok(followExecuted);
        }
    
        @GetMapping("/commons/{targetUserId}")
        public Result followCommons(@PathVariable Long targetUserId) {
            List<UserDTO> commons = followService.followCommons(targetUserId);
            return Result.ok(commons);
        }
    }
  4. Service接口:

    复制代码
    public interface IFollowService extends IService<Follow> {
        Boolean isFollow(Long followUserId);
        Boolean follow(Long followUserId);
        List<UserDTO> followCommons(Long id);
    }
  5. ServiceImpl 类:

    复制代码
    @Service
    public class FollowServiceImpl extends ServiceImpl<FollowMapper, Follow> implements IFollowService {
    
        @Resource
        private StringRedisTemplate stringRedisTemplate;
        
        @Resource
        private IUserService userService;
    
        @Override
        public Boolean isFollow(Long followUserId) {
            // 获取当前用户id
            Long userId = UserHolder.getUser().getId();
            String key = "follow:" + userId;
            // 缓存不为空,则直接查询用户关注列表
            if (stringRedisTemplate.hasKey(key)) {
        				return stringRedisTemplate.opsForSet().isMember(key, followUserId.toString());
    		    }
    		    // 缓存为空时,从数据库加载用户关注列表
    		    List<Long> followIds = baseMapper.selectFollowedIds(userId);
            // 没有关注的博主,则缓存空对象(防止缓存穿透)
    		    if (followIds.isEmpty()) {
    		        stringRedisTemplate.opsForSet().add(key, "null");        // 缓存空对象
    		        stringRedisTemplate.expire(key, 10, TimeUnit.MINUTES);   // 设置失效时间
    		        return false;
    		    }
    		    // followIds.forEach(id -> stringRedisTemplate.opsForSet().add(key, id.toString()));
    		    stringRedisTemplate.opsForSet().add(key, followIds.stream()
    					  .map(String::valueOf)
    					  .toArray(String[]::new));
            stringRedisTemplate.expire(key, 60, TimeUnit.MINUTES);        // 设置失效时间
    				return stringRedisTemplate.opsForSet().isMember(key, followUserId.toString());
        }
    
        @Override
        public Boolean follow(Long followUserId) {
            Long userId = UserHolder.getUser().getId();
    		    Boolean isFollowed = isFollow(followUserId);
    		    Boolean success = false;
            
            if (!isFollowed) {
    		        // 未关注 => 关注操作
                Follow follow = new Follow();
                follow.setUserId(userId);
                follow.setFollowUserId(followUserId);
                success = save(follow);
                if (success) {
                    stringRedisTemplate.opsForSet().add(key, followUserId.toString());
                }
            } else {
                // 已关注 => 取关操作
                success = remove(new QueryWrapper<Follow>()
                        .eq("user_id", userId)
                        .eq("follow_user_id", followUserId));
                if (success) {
                    stringRedisTemplate.opsForSet().remove(key, followUserId.toString());
                }
            }
            return success;
        }
    
        @Override
        public List<UserDTO> followCommons(Long targetUserId) {
            Long userId = UserHolder.getUser().getId();
            String key1 = "follow:" + userId;
            String key2 = "follow:" + targetUserId;
            // 求交集
            Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key1, key2);
            if (intersect == null || intersect.isEmpty()) {
                return Collections.emptyList();
            }
            // 解析id
            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 users;
        }
    }
  6. FollowMapper

    复制代码
    @Mapper
    public interface FollowMapper extends BaseMapper<Follow> {
    
        // 注解方式
        @Select("select follow_user_id from follow where user_id = #{userId}")
        List<Long> selectFollowedIds(Long userId);
        
    }
相关推荐
rit843249920 小时前
在Ubuntu上配置Nginx实现开机自启功能
数据库·nginx·ubuntu
小O的算法实验室20 小时前
2025年TRE SCI1区TOP,随机环境下无人机应急医疗接送与配送的先进混合方法,深度解析+性能实测
算法·论文复现·智能算法·智能算法改进
9号达人20 小时前
普通公司对账系统的现实困境与解决方案
java·后端·面试
超级苦力怕20 小时前
Java 为何 long a = 999999999 能过;long a = 9999999999 报错?一文讲透“宽化转换”
java
佐杰20 小时前
Jenkins使用指南1
java·运维·jenkins
dllxhcjla20 小时前
三大特性+盒子模型
java·前端·css
海绵啵啵呀20 小时前
SQL plus中解决上下键找历史命令的工具--rlwrap命令行工具
数据库·sql
小白程序员成长日记20 小时前
2025.11.06 力扣每日一题
算法·leetcode
Acrelhuang20 小时前
筑牢用电防线:Acrel-1000 自动化系统赋能 35kV 园区高效供电-安科瑞黄安南
java·大数据·开发语言·人工智能·物联网
Elastic 中国社区官方博客20 小时前
使用 Mastra 和 Elasticsearch 构建具有语义回忆功能的知识 agent
大数据·数据库·人工智能·elasticsearch·搜索引擎·ai·全文检索