Set 类型
定义
:类似 Java 中的 HashSet 类,key 是 set 的名字,value 是集合中的值特点
- 无序
- 元素唯一
- 查找速度快
- 支持交集、并集、补集功能
常见命令
命令 | 功能 |
---|---|
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 集合的并集 |
编码方式
IntSet 编码
- 定义:IntSet 是一个有序的整数数组结构,相比哈希表占用更少的内存空间
- 使用条件:当集合中存储的所有数据都是整数,并且元素数量不超过配置项 set-max-intset-entries(默认值为512)
- 功能:满足使用条件时 Redis 自动使用 IntSet 编码,减少内存占用
HT(Hash Table)编码
- 定义:key 存储集合的元素,value 统一设置为 null(因为 Set 只关心元素是否存在,不需要存储值)
- 使用条件:当不满足 IntSet 编码条件时,Redis 会使用哈希表来存储集合
- 功能:提供快速的查找性能,但需要消耗更多内存
示例
- 目标:用户关注与取关博主,查看用户与博主的共同关注
- 注意:此处代码实现涉及较多 SpringBoot 和 MybatisPlus 相关知识,已默认读者有一定基础
功能点
- 判断当前登录用户是否已经关注当前博主
- 当前用户关注 & 取关当前博主
- 查询当前用户与当前博主的共同关注
业务方案
Set 类型(Redis)
-
功能
:记录当前用户关注的所有博主,并且可以查看共同关注(交集操作) -
数据结构
:Setkey value (set) follow:userId(prefix + 做出关注行为的用户 id) 被 key 用户关注的用户 id 集合
MySQL
-
功能
- 记录所有关注与被关注关系:创建一个关注表,每个条目对应一个关注关系
- 查询是否关注:select * from subscribe_table where user_id = userId and follow_user_id followUserId (查询结果不为空则已关注)
- 查询关注列表:select follow_user_id from subscribe_table where user_id = userId (查询结果是所有 userId 关注的博主的id)
-
数据结构
字段名 功能 id primary key (自增) user_id 做出关注行为的用户的 ID follow_user_id 被关注的用户的 ID create_time 创建时间
最终方案
- 利用 Redis Set 的快速查询某个用户是否已经关注另一个用户
- 利用 Redis Set 的交集操作快速实现共同关注功能
- 利用 MySQL 的 follow 表完整记录并持久化所有关注与被关注的关系
- 使用 MySQL 存储关注关系的基础数据,并使用Redis Set来提升共同关注等高频查询场景的性能
代码实现
-
配置文件
-
目标:自动移除非活跃用户的关注列表,下次访问时再通过 MySQL 重建缓存
-
方案:使用 LRU(Least Recently Used)缓存淘汰策略。当内存超出阈值时,自动淘汰最久未使用的数据
-
注意:需要为 follow 缓存设置独立的 key 前缀,并结合 maxmemory-policy 配置分区缓存策略,避免误删其他缓存数据
maxmemory-policy allkeys-lru
-
-
实体类 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; }
-
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); } }
-
Service接口:
public interface IFollowService extends IService<Follow> { Boolean isFollow(Long followUserId); Boolean follow(Long followUserId); List<UserDTO> followCommons(Long id); }
-
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; } }
-
FollowMapper
@Mapper public interface FollowMapper extends BaseMapper<Follow> { // 注解方式 @Select("select follow_user_id from follow where user_id = #{userId}") List<Long> selectFollowedIds(Long userId); }