查看原文
其他

用Redis的zset有序集合实现一个本周热议功能

吕一明 MarkerHub 2022-11-04

小Hub领读:

项目eblog的第四篇讲解文章,今天我们先来实现一个比较牛逼的功能,博客里面有个本周热议排行功能,就是根据评论的数量,来给文章排行,并且限定是本周评论最多的文章。如果考虑评论的人数会很多,比如像微博那么大流量,那么使用Redis作为缓存,如何来做一个排行榜呢?看看今天的文章哈!


项目名称:eblog

项目 Git 仓库:https://github.com/MarkerHub/eblog(给个 star 支持哈)

前几篇项目讲解文章:

1、Github 上最值得学习的 Springboot 开源博客项目!

2、小 Hub 手把手教你如何从 0 搭建一个开源项目架构

3、整合Redis,以及项目优雅的异常处理与返回结果封装


本周热议,本周发表并且评论最多的文章排行,如果直接查询数据库的话很快就可以实现,只需要限定一下文章创建时间,然后更加评论数量倒叙取前几篇即可搞定。

但这里我们使用redis来完成。之前上课时候我们说过,排行榜功能,我们可以使用redis的有序集合zset来完成。现在我们就这个数据结构来完成本周热议的功能。

在编码之前,我们需要先来回顾一下zset的几个基本命令。

  • zrange key start stop [WITHSCORES]

withscores代表的是否显示顺序号  start和stop代表所在的位置的索引。可以这样理解:将集合元素依照顺序值升序排序再输出,start和stop限制遍历的限制范围

  • zincrby key increment member

为有序集 key 的成员 member 的 score 值加上增量 increment 。

  • ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]

计算给定的一个或多个有序集的并集,其中给定 key 的数量必须以 numkeys 参数指定,并将该并集(结果集)储存到 destination 。

默认情况下,结果集中某个成员的 score 值是所有给定集下该成员 score 值之 和 。

其他命令可以参考这里:http://doc.redisfans.com/

以下是我做的实验:

我们来分析一下我们的需求。我们想用缓存来完成这本周热议排行榜功能,不依赖数据库(除了初始化数据)。有人发表评论之后,直接使用命令加一,并重新计算并集得到排行榜。

项目启动时候我们先初始化最近文章的评论数量。基本逻辑如下:

  1. 查库获取最近7天的所有文章(或者加多一个条件:评论数量大于0)

  2. 然后把文章的评论数量作为有序集合的分数,文章id作为ID存储到zset中。

  3. 本周热议上有标题和评论数量,因此,我们还需要把文章的基本信息存储到redis总。这样得到文章的id之后,我们再从缓存中得到标题等信息,这里我们可以使用hash的结构来存储文章的信息。

  4. 另外,因为是本周热议,如果文章发表超过7天了之后就没啥用了,所以我们可以给文章的有序集合一个有效时间。超过7天之后就自定删除缓存。

具体代码如下:

  • com.example.service.impl.PostServiceImpl#initIndexWeekRank

  1. /**

  2. * 初始化首页的周评论排行榜

  3. */

  4. @Override

  5. public void initIndexWeekRank() {

  6. //缓存最近7天的文章评论数量

  7. List<Post> last7DayPosts = this.list(new QueryWrapper<Post>()

  8. .ge("created", DateUtil.offsetDay(new Date(), -7).toJdkDate())

  9. .select("id, title, user_id, comment_count, view_count, created"));

  10. for (Post post : last7DayPosts) {

  11. String key = "day_rank:" + DateUtil.format(post.getCreated(), DatePattern.PURE_DATE_PATTERN);

  12. //设置有效期

  13. long between = DateUtil.between(new Date(), post.getCreated(), DateUnit.DAY);

  14. long expireTime = (7 - between) * 24 * 60 * 60;

  15. //缓存文章到set中,评论数量作为排行标准

  16. redisUtil.zSet(key, post.getId(), post.getCommentCount());

  17. //设置有效期

  18. redisUtil.expire(key, expireTime);

  19. //缓存文章基本信息(hash结构)

  20. this.hashCachePostIdAndTitle(post);

  21. }

  22. //7天阅读相加。

  23. this.zUnionAndStoreLast7DaysForLastWeekRank();

  24. }

  • 对应的缓存文章信息的方法如下:

  1. /**

  2. * hash结构缓存文章标题和id

  3. * @param post

  4. */

  5. private void hashCachePostIdAndTitle(Post post) {

  6. boolean isExist = redisUtil.hasKey("rank_post_" + post.getId());

  7. if(!isExist) {

  8. long between = DateUtil.between(new Date(), post.getCreated(), DateUnit.DAY);

  9. long expireTime = (7 - between) * 24 * 60 * 60;

  10. //缓存文章基本信息(hash结构)

  11. redisUtil.hset("rank_post_" + post.getId(), "post:id", post.getId(), expireTime);

  12. redisUtil.hset("rank_post_" + post.getId(), "post:title", post.getTitle(), expireTime);

  13. //redisUtil.hset("rank_post_" + post.getId(), "post:comment_count", post.getCommentCount(), expireTime);

  14. }

  15. }

  • 统计7天的文章集合交集数量:

  1. /**

  2. * 把最近7天的文章评论数量统计一下

  3. * 用于首页的7天评论排行榜

  4. */

  5. public void zUnionAndStoreLast7DaysForLastWeekRank() {

  6. String prifix = "day_rank:";

  7. List<String> keys = new ArrayList<>();

  8. String key = prifix + DateUtil.format(new Date(), DatePattern.PURE_DATE_PATTERN);

  9. for(int i = -7 ; i < 0; i++) {

  10. Date date = DateUtil.offsetDay(new Date(), i).toJdkDate();

  11. keys.add(prifix + DateUtil.format(date, DatePattern.PURE_DATE_PATTERN));

  12. }

  13. redisUtil.zUnionAndStore(key, keys, "last_week_rank");

  14. }

写好了之后,我们再我们的项目启动类中调用一下即可完成了初始化。

  1. @Slf4j

  2. @Order(1000)

  3. @Component

  4. public class ContextStartup implements ApplicationRunner, ServletContextAware {


  5. private ServletContext servletContext;


  6. @Autowired

  7. PostService postService;


  8. @Override

  9. public void setServletContext(ServletContext servletContext) {

  10. this.servletContext = servletContext;

  11. }


  12. @Override

  13. public void run(ApplicationArguments args) throws Exception {


  14. servletContext.setAttribute("base", servletContext.getContextPath());


  15. //初始化首页的周评论排行榜

  16. postService.initIndexWeekRank();


  17. }

  18. }

以上就完成了初始化。这时候我们在本周热议模块已经可以看到效果了。缓存中已经有我们想要的数据,接下我们在controller中获取出来,然后返回给个我们的页面,页面用异步加载的模式,所以这里定义一个异步接口:

  • com.example.controller.PostController

  1. @ResponseBody

  2. @GetMapping("/post/hots")

  3. public Result hotPost() {


  4. Set<ZSetOperations.TypedTuple> lastWeekRank = redisUtil.getZSetRank("last_week_rank", 0, 6);


  5. List<Map<String, Object>> hotPosts = new ArrayList<>();

  6. for (ZSetOperations.TypedTuple typedTuple : lastWeekRank) {


  7. Map<String, Object> map = new HashMap<>();

  8. map.put("comment_count", typedTuple.getScore());

  9. map.put("id", redisUtil.hget("rank_post_" + typedTuple.getValue(), "post:id"));

  10. map.put("title", redisUtil.hget("rank_post_" + typedTuple.getValue(), "post:title"));


  11. hotPosts.add(map);

  12. }


  13. return Result.succ(hotPosts);

}

测试结果:

致此,我们已经完成了获取本周热议的数据,但是,只是一个初始化而已,当有评论的时候还应该添加数据到我们的缓存中,还有页面的内容我们也应该写一些ajax加载数据,这些我们先留着,先到这里,以上是我们之前课程讲过的内容,大家先行消化一下。

(完)

MarkerHub文章索引:

https://github.com/MarkerHub/JavaIndex


【项目相关文章】

1、Github上最值得学习的Springboot开源博客项目!

2、小Hub手把手教你如何从0搭建一个开源项目架构

3、整合Redis,以及项目优雅的异常处理与返回结果封装




 给eblog一个star,感谢支持哈

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存