本文详细介绍了如何使用 Redis 的 SortedSet 数据结构实现一个基于时间顺序的博客点赞排行榜系统。
SortedSet
-
定义
:Redis 中的有序集合- key:集合的名字 (此处业务逻辑中是 Blog 对象名)
- field:集合成员对象 (此处业务逻辑中使用 SortedSet,其中 score 为点赞时间 )
- score:集合成员对象用于排序的依据 (此处业务逻辑中使用点赞时间 System.currentTimeMillis() 作为排序依据)
-
功能
- 元素唯一:存储点赞当前 Blog 的用户集合,避免重复点赞
- 根据 score 排序:存储点赞时间,需要时可以按照点赞时间顺序进行排序
- 根据 field 查找:可以判断当前登录用户是否已经点赞当前 Blog
-
常用命令
-
添加:ZADD key score1 elem1 score2 elem2 …
-
单个查询:ZSCORE key myElem
- 注:实际上是在查询 field 的 score 值,如果 score 为 null 则说明不存在 myElem 元素
-
范围查询:ZRANGE key myLowRank myHighRank
-
示例:点赞排行榜
功能点
- 当前登录用户对 Blog 进行点赞
- 当前登录用户对 Blog 取消点赞
- 判断当前用户是否已经对 Blog 点赞
- 查询最早点赞当前 Blog 的 5 个用户
业务逻辑
业务方案
数据结构:SortedSet
增加表的字段:@TableField(exist = false)
-
存储位置
:逻辑属于 MySQL 数据库的字段,实际上字段并不存在 -
定义
:MySQL 中的一个虚拟字段,可以给其赋值但是不会真正写入数据库 -
功能
:判断当前登录用户是否已经给当前 Blog 点赞 -
实现逻辑
- 在 Redis 中查找已经点赞当前 Blog 的用户集合 Set
- 判断 Set 中是否存在当前登录用户
- 如果存在,则 Blog 的 isLike 属性修改为 true
- 如果不存在,则 Blog 的 isLike 属性修改为 false
代码实现
-
BlogController
@GetMapping("/likes/{id}") public Result queryBlogLikes(@PathVariable("id") Long blogId) { return blogService.queryBlogLikes(blogId);
-
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); }
方案优化
-
设置过期时间
- 为每个博客的点赞集合设置合理的过期时间
- 只保留最近一段时间(如 7 天或 30 天)的点赞记录
-
分层存储策略
- 热门博客的点赞数据保存在 Redis 中
- 冷门博客的点赞数据迁移到数据库中
- 根据访问频率动态调整存储策略