案例需求

方案分析

一个用户对于一个 blog 只能点赞一次,如果只使用数据库完成该功能,那么用户每点赞一次都要查询、修改数据库,这样无疑增加了数据库的压力,那么引入 redis,就可以使用 redis 的 Sorted_Set 集合解决该问题(中间件的引入当然是越少越好的,这样有利于系统的稳定性,那此处为了场景演示就引入 redis 了,毕竟现在有点规模的项目基本都离不开 redis)
redis 的 Sorted_Set 集合 集合中的元素具有唯一性、有序性,利用唯一性完成一个用户对于一个 blog 只能点赞一次,利用有序性完成点赞 top5 的用户排序,key 是 blog 的 id,value 是用户 id 和用于排序的时间戳,如果该 key 的 value 中有当前登录用户的 userId,那么说明该用户对该 blog 已点赞,那么可以利用该用户的时间戳进行排序,如果没有当前登陆用户的 userId,则未点赞。
点赞功能代码实现
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
|
@Override
public Result likeBlog(Long id) {
//获取当前用户
Long loginUserId = UserHolder.getUser().getId();
//判断该用户是否点赞
String key="blog:liked:"+id;
Double score = stringRedisTemplate.opsForZSet().score(key, loginUserId.toString());
//score为空说明该用户未点赞
if (score==null){
//如果未点赞则点赞成功,数据库点赞数+1,用户对该blog的点赞信息添加到redis的set
boolean isUpdate = update().setSql("liked = liked + 1").eq("id", id).update();
if (isUpdate){
//opsForZSet使用的是scoreSet集合,该集合较于set集合,可以对元素进行排序,基于该特性可以对点赞用户的顺序进行排名
stringRedisTemplate.opsForZSet().add(key,loginUserId.toString(),System.currentTimeMillis());
}
}else {
//如果已点赞则取消点赞,数据库点赞数-1,用户对该blog的点赞信息从redis的set移除
boolean isUpdate = update().setSql("liked = liked - 1").eq("id", id).update();
if (isUpdate){
stringRedisTemplate.opsForZSet().remove(key,loginUserId.toString());
}
}
return Result.ok();
}
|
最早点赞 top5 功能代码实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
@Override
public Result queryBlogLikes(Long id) {
//从redis查询top5的点赞用户
Set<String> top5 = stringRedisTemplate.opsForZSet().range("blog:liked:" + id, 0, 4);
if (top5==null||top5.isEmpty())
{
return Result.ok(Collections.emptyList());
}
//从查询的数据中得到top5的用户id
List<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());
String idsStr = StrUtil.join(",", ids);
//根据id查询top5的用户并返回
// List<User> users = userService.listByIds(ids);
//详见黑马点评 实战篇 P10
List<User> users = userService.query().in("id", ids).last("ORDER BY FIELD (id," + idsStr + ")").list();
List<UserDTO> userDTOS = users.stream()
.map(user -> BeanUtil.copyProperties(user, UserDTO.class))
.collect(Collectors.toList());
return Result.ok(userDTOS);
}
|
细节注意
在最初使用 userService.listByIds(ids)查询数据库用户时,查询条件中的 id 顺序和查询结果的顺序是相反的,具体效果如下:
先查 id=5,再查 id=1,结果是 id=1 在前面,id=5 在后面,这样导致最早点赞 top5 的用户顺序相反

为解决上述问题,使用 order by 对 id 排序

注意这里演示的 id 是确定的数值,在实际代码中要把查询到的用户 id 列表转换成动态字符串,如下:
1
2
3
4
5
6
7
8
|
//从查询的数据中得到top5的用户id
List<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());
String idsStr = StrUtil.join(",", ids);
List<User> users = userService.query().in("id", ids).last("ORDER BY FIELD (id," + idsStr + ")").list();
List<UserDTO> userDTOS = users.stream()
.map(user -> BeanUtil.copyProperties(user, UserDTO.class))
.collect(Collectors.toList());
|
Bug 修正
redis 报错
1
|
WRONGTYPE Operation against a key holding the wrong kind of value
|
原因是由于之前点赞功能是使用 set 集合,使得 redis 中已经存在了一个名为"blog:liked:23"的 key 值,添加点赞 top5 功能后由于把 set 集合换成了 sorted_set 集合导致 key 冲突,解决方案是删掉之前的 key 值