Redis - 顺序队列 SortedSet

46 阅读3分钟

本文详细介绍了如何使用 Redis 的 SortedSet 数据结构实现一个基于时间顺序的博客点赞排行榜系统。

SortedSet

  1. 定义:Redis 中的有序集合

    1. key:集合的名字 (此处业务逻辑中是 Blog 对象名)
    2. field:集合成员对象 (此处业务逻辑中使用 SortedSet,其中 score 为点赞时间 )
    3. score:集合成员对象用于排序的依据 (此处业务逻辑中使用点赞时间 System.currentTimeMillis() 作为排序依据)
  2. 功能

    1. 元素唯一:存储点赞当前 Blog 的用户集合,避免重复点赞
    2. 根据 score 排序:存储点赞时间,需要时可以按照点赞时间顺序进行排序
    3. 根据 field 查找:可以判断当前登录用户是否已经点赞当前 Blog
  3. 常用命令

    1. 添加:ZADD key score1 elem1 score2 elem2 …

    2. 单个查询:ZSCORE key myElem

      1. 注:实际上是在查询 field 的 score 值,如果 score 为 null 则说明不存在 myElem 元素
    3. 范围查询:ZRANGE key myLowRank myHighRank


示例:点赞排行榜

功能点

  1. 当前登录用户对 Blog 进行点赞
  2. 当前登录用户对 Blog 取消点赞
  3. 判断当前用户是否已经对 Blog 点赞
  4. 查询最早点赞当前 Blog 的 5 个用户

业务逻辑

业务方案

数据结构:SortedSet

增加表的字段:@TableField(exist = false)

  1. 存储位置:逻辑属于 MySQL 数据库的字段,实际上字段并不存在

  2. 定义:MySQL 中的一个虚拟字段,可以给其赋值但是不会真正写入数据库

  3. 功能:判断当前登录用户是否已经给当前 Blog 点赞

  4. 实现逻辑

    1. 在 Redis 中查找已经点赞当前 Blog 的用户集合 Set
    2. 判断 Set 中是否存在当前登录用户
    3. 如果存在,则 Blog 的 isLike 属性修改为 true
    4. 如果不存在,则 Blog 的 isLike 属性修改为 false

代码实现

  1. BlogController

    @GetMapping("/likes/{id}")
    public Result queryBlogLikes(@PathVariable("id") Long blogId) {
    		return blogService.queryBlogLikes(blogId);
    
  2. BlogServiceImpl

    @Override
    public Result likeBlog(Long blogId) {
    		Long userId = UserHolder.getUser().getId();
    		String key = BLOG_LIKED_KEY + blogId;
    		Double score = stringRedisTemplate.opsForZset().score(key, userId.toString());
    		
    		// 当前用户没有点赞 -> 点赞
    		if(score != null) {
    				boolean updateSuccess = update().setSql("like = like + 1").eq("id", blogId).update();
    				if(updateSuccess)  {
    						stringRedisTemplate.opsForZset().put(key, userId.toString(), System.currentTimeMillis());
    				}
    				return Result.ok();
    		} else {
    		// 当前用户已经点赞 -> 取消点赞
    				boolean updateSuccess = update().setSql("liked = liked - 1").eq("id", blogId).update();
    				if(updateSuccess) {
    						stringRedisTemplate.opsForZset().remove(key, userId.toString());
    				}
    				return Result.ok();
    		}
    }
    
    @Override
    public Result queryBlogLikes(Long blogId) {
    		String key = BLOG_LIKED_KEY + blogId;
    		// 查询最早点赞的 5 个用户的 id (String 类型)
    		Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);
    		if(top5 == null || top5.isEmpty())
    				return Result.ok(Collections.emptyList());
    		// 获取 5 个用户的 Long 类型 id 并转为 List 数组
    		List<Long> userIds = top5.stream().map(Long::valueOf).collect(Collectors.toList());
    		// 查询 5 个用户的对象并转为 DTO 返回
    		String idStr = StrUtil.join(",", userIds);
    		List<UserDTO> userDTOS = userService.query()
    					.in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list()
    					.stream().map(user -> BeanUtil.copyProperties(user, UserDTO.class).collect(Collectors.toList());
    		return Result.ok(userDTOS);
    }
    
    private void isBlogLiked(Blog blog) {
    		// 获取登录用户(因为不登录也可以看这个界面,所以需要处理用户未登录的情况)
    		UserDTO user = UserHolder.getUser();
    		if(user = null) return;
    		Long userId = user.getId();
    		
    		String key = BLOG_LIKED_KEY + blog.getId();
    		Double score = stringRedisTemplate.opsForZset().score(key, userId.toString());
    		blog.setIsLike(score != null);
    }
    

方案优化

  1. 设置过期时间

    • 为每个博客的点赞集合设置合理的过期时间
    • 只保留最近一段时间(如 7 天或 30 天)的点赞记录
  2. 分层存储策略

    • 热门博客的点赞数据保存在 Redis 中
    • 冷门博客的点赞数据迁移到数据库中
    • 根据访问频率动态调整存储策略