Redis实践之点赞排行榜

Redis实践之点赞排行榜

1.项目背景

探店笔记类似点评网站的评价,往往是图文结合。对应的表有两个:

  • tb_blog:探店笔记表,包含笔记中的标题、文字、图片等
  • tb_blog_comments:其他用户对探店笔记的评价

image-20240111105725776

image-20240111105753513

涉及的相关接口包括上传图片接口、发布笔记接口、查看笔记详情接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public Result uploadImage(@RequestParam("file") MultipartFile image) {
try {
// 获取原始文件名称
String originalFilename = image.getOriginalFilename();
// 生成新文件名
String fileName = createNewFileName(originalFilename);
// 保存文件
image.transferTo(new File(SystemConstants.IMAGE_UPLOAD_DIR, fileName));
// 返回结果
log.debug("文件上传成功,{}", fileName);
return Result.ok(fileName);
} catch (IOException e) {
throw new RuntimeException("文件上传失败", e);
}
}

private String createNewFileName(String originalFilename) {
// 获取后缀
String suffix = StrUtil.subAfter(originalFilename, ".", true);
// 生成目录
String name = UUID.randomUUID().toString();
int hash = name.hashCode();
int d1 = hash & 0xF;
int d2 = (hash >> 4) & 0xF;
// 判断目录是否存在
File dir = new File(SystemConstants.IMAGE_UPLOAD_DIR, StrUtil.format("/blogs/{}/{}", d1, d2));
if (!dir.exists()) {
dir.mkdirs();
}
// 生成文件名
return StrUtil.format("/blogs/{}/{}/{}.{}", d1, d2, name, suffix);
}

public Result saveBlog(@RequestBody Blog blog) {
// 获取登录用户
UserDTO user = UserHolder.getUser();
blog.setUserId(user.getId());
// 保存探店博文
blogService.save(blog);
// 返回id
return Result.ok(blog.getId());
}

public Result queryBlogById(Long id) {
// 查询笔记
Blog blog = getById(id);
if (blog == null) {
return Result.fail("笔记不存在!");
}
// 注入用户信息
Long userId = blog.getUserId();
User user = userService.getById(userId);
blog.setName(user.getNickName());
blog.setIcon(user.getIcon());
// 判断当前用户是否已点赞
blog.setIsLike(isMember(blog, userId));
return Result.ok(blog);
}

private Boolean isMember(Blog blog, Long userId) {
String key = "blog:liked:" + blog.getId();
return stringRedisTemplate.opsForZSet().score(key, userId.toString()) != null;
}

前端需要知道当前用户是否已点赞过某篇笔记,以便高亮显示,同时需要保证一人一赞功能,重复点赞等同于取消点赞,这些依赖于Redis中的sorted set数据结构实现(保存某篇笔记的点赞用户列表,按照点赞时间戳排序)。

2.点赞功能

image-20240111110608746

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public Result likeBlog(Long id) {
// 查询当前用户是否已点赞
String key = "blog:liked:" + id;
Long userId = UserHolder.getUser().getId();
Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
// 当前用户未点赞
if (score == null) {
// 数据库点赞数+1
boolean updated = update().setSql("liked = liked + 1").eq("id", id).update();
if (updated) {
// Redis笔记点赞列表添加当前用户
stringRedisTemplate.opsForZSet().add(key, userId.toString(), System.currentTimeMillis());
}
} else { // 当前用户已点赞
// 数据库点赞数-1
boolean updated = update().setSql("liked = liked - 1").eq("id", id).update();
if (updated) {
// Redis笔记点赞列表删除当前用户
stringRedisTemplate.opsForZSet().remove(key, userId.toString());
}
}
return Result.ok();
}

3.点赞排行榜

image-20240111110752093

image-20240111110808582

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public Result queryBlogLikes(Long id) {
String key = "blog:liked:" + id;
// 1.查询top5的点赞用户
Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);
if (top5 == null || top5.isEmpty()) {
return Result.ok(Collections.emptyList());
}
// 2.解析出其中的用户id
List<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());
String idStr = StrUtil.join(",", ids);
// 3.根据用户id查询用户 WHERE id IN ( 5 , 1 ) ORDER BY FIELD(id, 5, 1)
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());
// 4.返回
return Result.ok(userDTOS);
}