6. 集成 Redis 实现本周热议
6.1 环境搭建
- 添加 redis 依赖,使用 utils 包下的 RedisUtil 对内置 RedisTemplate 进行封装
- 添加 hutool 依赖,使用 DateUtil 类中的 offsetDay()、format()
- 考虑到 redis 序列化后出现乱码问题,使用 RedisConfig 配置类进行编码的处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14<dependencies>
<!--Redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--hutool:工具包,例如DateUtils工具类...-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>4.1.17</version>
</dependency>
</dependencies>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24/**
* 指定Redis的序列化后的格式
*/
public class RedisConfig {
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
jackson2JsonRedisSerializer.setObjectMapper(new ObjectMapper());
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}
}
6.2 本周热议的【基本原理】:利用 Redis 的 zet 有序集合实现
- 缓存热评文章——哈希表 Hash
- 评论数量排行——有序列表 sortedSet:ZADD(添加)、ZREVRANGE(展示)、ZUNIONSTORE(并集)
- ZADD key score member [[score member] [score member] …]
1
2
3
4
5
6
7
8
9
10
11
12
13
14127.0.0.1:6379> ZADD day:18 10 post:1 6 post:2 4 post:3
(integer) 3
127.0.0.1:6379> ZADD day:19 10 post:1 6 post:2 4 post:3
(integer) 3
127.0.0.1:6379> ZADD day:20 10 post:1 6 post:2 4 post:3
(integer) 3
127.0.0.1:6379> ZADD day:21 10 post:1 6 post:2 4 post:3
(integer) 3
127.0.0.1:6379> ZADD day:22 10 post:1 6 post:2 4 post:3
(integer) 3
127.0.0.1:6379> ZADD day:23 10 post:1 6 post:2 4 post:3
(integer) 3
127.0.0.1:6379> ZADD day:24 10 post:1 6 post:2 4 post:3
(integer) 3 - ZREVRANGE key start stop [WITHSCORES]
1
2
3
4
5
6
7127.0.0.1:6379> ZREVRANGE day:18 0 -1 withscores
1) "post:1"
2) "10"
3) "post:2"
4) "6"
5) "post:3"
6) "4" - ZUNIONSTORE destination numkeys key [key …] [WEIGHTS weight [weight …]] [AGGREGATE SUM|MIN|MAX]
1
2
3127.0.0.1:6379> ZUNIONSTORE week:rank 7 day:18 day:19 day:20 day:21 day:22 day:23 day:24
1) "post:1"
2) "post:2" - 查看排行榜
1
2
3
4
5
6
7127.0.0.1:6379> ZREVRANGE week:rank 0 -1 withscores
1) "post:1"
2) "70"
3) "post:2"
4) "42"
5) "post:3"
6) "28" - 添加/删除评论
1
2
3
4
5
6
7
8
9
10
11127.0.0.1:6379> ZINCRBY day:18 10 post:1
"20"
127.0.0.1:6379> ZREVRANGE day:18 0 -1 withscores
1) "post:1"
2) "20"
3) "post:2"
4) "6"
5) "post:3"
6) "4"
127.0.0.1:6379> ZINCRBY day:18 -10 post:1
"10"
- ZADD key score member [[score member] [score member] …]
6.3 本周热议的【初始化操作】
- 项目启动前,获取【近 7 天文章】
- 初始化【近 7 天文章】的总评论量(先使用 SortedSet 集合对【排行榜 7 天内全部文章】进行 zadd 操作,并设置它们 expire 为 7 天;再使用 Hash 哈希表对【排行榜 7 天内全部文章】进行 hexists 判断,再 hset 缓存操作)
- 添加 add——将【近 7 天文章】创建日期时间作为 key 值,每篇文章对应的 id 作为它的 value 值,每篇文章对应的评论 comment 作为它的 score 值,并使用 redis 的工具类(RedisUtil),对文章的具体属性进行 zSet()缓存操作
- 过期 expire——让【近 7 天文章】的 key 过期: 7-(当前时间-创建时间)= 过期时间
- 缓存——缓存【近 7 天文章】的一些基本信息,例如文章 id,标题 title,评论数量,作者信息…方便访问【近 7 天文章】时,直接 redis,而非 MySQL
- 先对文章进行 EXISTS 判断其缓存是否存在
- 如果 false 不存在,则再 hset 缓存操作
- 对【近 7 天文章】做并集运算(zUnionAndStore), 并使用根据评论量的数量从大到小进行展示(zrevrange)
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/**
* Context配置类
*/
public class ContextStartup implements ApplicationRunner, ServletContextAware {
CategoryService categoryService;
ServletContext servletContext;
PostService postService;
/**
* 项目启动时,会同时调用该run方法:
*
* 加载导航栏中的“提问、分享、讨论、建议”,并将其list放入servletContext上下文对象
* 加载本周热议
*/
public void run(ApplicationArguments args) throws Exception {
List<Category> categories = categoryService.list(new QueryWrapper<Category>()
.eq("status", 0)
);
servletContext.setAttribute("categorys", categories);
postService.initWeekRank();
}
/**
* servletContext上下文对象
*/
public void setServletContext(ServletContext servletContext) {
this.servletContext = servletContext;
}
}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
public class PostServiceImpl extends ServiceImpl<PostMapper, Post> implements PostService {
RedisUtil redisUtil;
/**
* 项目启动前,初始化本周热议(近7天全部文章评论量的排行榜)
*/
public void initWeekRank() {
//1.获取【近7天文章】
List<Post> posts = this.list(new QueryWrapper<Post>()
.gt("created", DateUtil.offsetDay(new Date(), -6)) //根据created时间,对最近7天内的文章进行筛选
.select("id, title, user_id, comment_count, view_count, created") //对文章的属性进行筛选,加快查询速率
);
//2.初始化【近7天文章】的总评论量(先使用SortedSet集合对【排行榜7天内全部文章】进行zadd操作,并设置它们expire为7天;再使用Hash哈希表对【排行榜7天内全部文章】进行hexists判断,再hset缓存操作)
for (Post post : posts) {
//1.添加add——|day:rank:20210202--0208|,将【近7天文章】创建日期时间作为key值,每篇文章对应的id作为它的value值,每篇文章对应的评论comment作为它的score值,并使用redis的工具类(RedisUtil),对文章的具体属性进行zSet()缓存操作
String zKey = "day:rank:" + DateUtil.format(post.getCreated(), DatePattern.PURE_DATE_FORMAT);
redisUtil.zSet(zKey, post.getId(), post.getCommentCount());//阅读redisUtil工具类,可知zSet等同于zadd
//2.过期expire——|day:rank:20210202--0208|,让【近7天文章】的key过期: 7-(当前时间-创建时间)= 过期时间
long expireTime = (7 - DateUtil.between(new Date(), post.getCreated(), DateUnit.DAY)) * 24 * 60 * 60;
redisUtil.expire(zKey, expireTime);
//3.缓存——|day:rank:post:1~16|,缓存【近7天文章】的一些基本信息,例如文章id,标题title,评论数量,作者信息...方便访问【近7天文章】时,直接redis,而非MySQL
//3.1先对文章进行EXISTS判断其缓存是否存在
String hKey = "day:rank:post:" + post.getId();
if (!redisUtil.hasKey(hKey)) {
//3.2如果false不存在,则再hset缓存操作
redisUtil.hset(hKey, "post-id", post.getId(), expireTime);
redisUtil.hset(hKey, "post-title", post.getTitle(), expireTime);
redisUtil.hset(hKey, "post-commentCount", post.getCommentCount(), expireTime);
redisUtil.hset(hKey, "post-viewCount", post.getViewCount(), expireTime);
}
}
//3.对【近7天文章】做并集运算(zUnionAndStore), 并使用根据评论量的数量从大到小进行展示(zrevrange)
String currentKey = "day:rank:" + DateUtil.format(new Date(), DatePattern.PURE_DATE_FORMAT);
List<String> otherKeys = new ArrayList<>();
for (int i = -6; i < 0; i++) {
String temp = "day:rank:" + DateUtil.format(DateUtil.offsetDay(new Date(), i), DatePattern.PURE_DATE_FORMAT);
otherKeys.add(temp);
}
String destKey = "week:rank";
redisUtil.zUnionAndStore(currentKey, otherKeys, destKey);
}
}
6.4 本周热议的【初始化操作】:自定义标签【hots】
1 | /** |
1 | /** |
6.4 本周热议的【更新操作】
- 自增/自减评论数
- 更新这篇文章的缓存时间,并更新这篇文章的基本信息
- 对【近 7 天文章】重新做并集运算(zUnionAndStore), 并使用根据评论量的数量从大到小进行展示(zrevrange)
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
public class PostServiceImpl extends ServiceImpl<PostMapper, Post> implements PostService {
RedisUtil redisUtil;
/**
* 本周热议:增加评论后,通过自增/自减评论数、再对排行榜做并集运算
*/
public void incrCommentCountAndUnionForWeekRank(Post post, boolean isIncr) {
//1.自增/自减评论数
String currentKey = "day:rank:" + DateUtil.format(new Date(), DatePattern.PURE_DATE_FORMAT);
redisUtil.zIncrementScore(currentKey, post.getId(), isIncr ? 1 : -1);
//2.更新这篇文章的缓存时间,并更新这篇文章的基本信息
String zKey = "day:rank:" + DateUtil.format(post.getCreated(), DatePattern.PURE_DATE_FORMAT);
long expireTime = (7 - DateUtil.between(new Date(), post.getCreated(), DateUnit.DAY)) * 24 * 60 * 60;
redisUtil.expire(zKey, expireTime);
String hKey = "day:rank:post:" + post.getId();
if (!redisUtil.hasKey(hKey)) {
//3.2如果false不存在,则再hset缓存操作
redisUtil.hset(hKey, "post-id", post.getId(), expireTime);
redisUtil.hset(hKey, "post-title", post.getTitle(), expireTime);
redisUtil.hset(hKey, "post-commentCount", post.getCommentCount(), expireTime);
redisUtil.hset(hKey, "post-viewCount", post.getViewCount(), expireTime);
}
//3.对【近7天文章】重新做并集运算(zUnionAndStore)
List<String> otherKeys = new ArrayList<>();
for (int i = -6; i < 0; i++) {
String temp = "day:rank:" + DateUtil.format(DateUtil.offsetDay(new Date(), i), DatePattern.PURE_DATE_FORMAT);
otherKeys.add(temp);
}
String destKey = "week:rank";
redisUtil.zUnionAndStore(currentKey, otherKeys, destKey);
}
}