美文网首页
面试官:Redis 如何实现每周热评功能?说说思路!

面试官:Redis 如何实现每周热评功能?说说思路!

作者: Kyriez7 | 来源:发表于2022-10-21 09:56 被阅读0次

    思路分析

    做每周热议,应该用缓存来做,如果直接查库的话,会对数据库造成压力。用缓存做的话,用Redis 来做缓存的话比较合适一点。

    # 利用Redsi 添加 数据命令
    # day:1 指的是在1号的时候 post:1 第一篇文章添加了 10 条评论。
    #后面 6 post:2 指的是 在1号第二篇添加了6条评论
    zadd day:1 10 post:1 6 post:2
    zadd day:2 10 post:1 6 post:2
    zadd day:3 10 post:1 6 post:2
    ....
    zadd day:8 10 post:1 6 post:2
    #这样就完成了7天的记录
    
    

    上面的命令可以帮我们记录一下7天的所有的评论数。但是还没有帮我们计算出来谁是评论最高的。看Redis 的sorted set有序集合有个命令就可以帮我们实现这个功能。

    这个命令可以帮助我们实现并集,我们只需要把7天的评论给做个并集就可以求出来。

    image.png
    # Redis 命令
    #意思是并集把这7天的 放到一个新的集合里面 新的集合是 week:rank 这样这个新的集合里面就有了我们的
    #7天的记录了
    union week:rank 7 day:1...day:8
    
    

    Redis 命令实践一下看看

    本地命令行测试

    image.png
    127.0.0.1:6379> ping
    PONG
    127.0.0.1:6379> zadd day:1 10 post:1
    (integer) 1
    127.0.0.1:6379> zadd day:2 10 post:1
    (integer) 1
    127.0.0.1:6379> zadd day:3 10 post:1
    (integer) 1
    127.0.0.1:6379> zadd day:1 5 post:2
    (integer) 1
    127.0.0.1:6379> zadd day:2 5 post:2
    (integer) 1
    127.0.0.1:6379> zadd day:3 5 post:2
    (integer) 1
    127.0.0.1:6379> keys *
    1) "day:1"
    2) "day:2"
    3) "day:3"
    
    

    查看当天的排行榜命令 ZRevrange

    127.0.0.1:6379> zrevrange day:1 0 -1 withscores
    1) "post:1"
    2) "10"
    3) "post:2"
    4) "5"
    127.0.0.1:6379>
    
    

    每周的评论排行榜记录。因为我只有三天的,所以只写了3天的

    127.0.0.1:6379> zrevrange week:rank 0 -1 withscores
    1) "post:1"
    2) "30"
    3) "post:2"
    4) "15"
    127.0.0.1:6379>
    
    

    上面的记录是没有错误的。上述的命令可以帮我们简单的实现了我们的想法,下面用代码来实现。上面还有一个小的 bug 就是当day:1这一天可能会出现就是不可能直接就过完了。可能会一条一条的增加,这个时候应该使用的是自增这个命令来解决这个问题。

    #什么时候+1 什么时候-1 就是当你 添加一条评论的时候就添加1 删除的的时候就减1
    ZINCRBY day:1 10 post:1
    
    

    代码来进行实现

    目前前端的样式,这样的话我们就需要在项目一开始的时候就启动这个功能

    image.png image.png
    @Component
    // 实现 启动类 ,还有 上下文的servlect
    public class ContextStartup implements ApplicationRunner, ServletContextAware {
        // 注入 categoryService
        @Autowired
        IMCategoryService categoryService;
        ServletContext servletContext;
        // 注入post 的服务类
        @Autowired
        IMPostService postService;
    
        @Override
        public void run(ApplicationArguments args) throws Exception {
            // 调用全查的方法
            List<MCategory> list = categoryService.list(new QueryWrapper<MCategory>().eq("status", 0));
            servletContext.setAttribute("List", list);
            // 调用每周热评的方法
            postService.initweek();
        }
    
        @Override
        public void setServletContext(ServletContext servletContext) {
            this.servletContext = servletContext;
        }
    }
    
    
    服务类serviceimpl类

    大概的思路

    • 获取7天内发表的文章

    • 初始化文章的总阅读量

      • 缓存文章的基本信息(id,标题,评论数,作者 ID )
      • 这样就可以避免的查库。可以直接用我们的缓存。
    • 做并集

    这里需要一个Redis 的工具类,我在网上找到的,不是我写的。网上一大堆。直接拿来用就可以了。

    推荐一个开源免费的 Spring Boot 最全教程:

    https://github.com/javastacks/spring-boot-best-practice

    package com.example.springbootblog.util;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.core.ZSetOperations;
    import org.springframework.stereotype.Component;
    import org.springframework.util.CollectionUtils;
    
    import java.util.Collection;
    import java.util.List;
    import java.util.Map;
    import java.util.Set;
    import java.util.concurrent.TimeUnit;
    
    @Component
    public class RedisUtil {
    
        @Autowired
        private RedisTemplate redisTemplate;
    
        /**
         * 指定缓存失效时间
         *
         * @param key  键
         * @param time 时间(秒)
         * @return
         */
        public boolean expire(String key, long time) {
            try {
                if (time > 0) {
                    redisTemplate.expire(key, time, TimeUnit.SECONDS);
                }
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 根据key 获取过期时间
         *
         * @param key 键 不能为null
         * @return 时间(秒) 返回0代表为永久有效
         */
        public long getExpire(String key) {
            return redisTemplate.getExpire(key, TimeUnit.SECONDS);
        }
    
        /**
         * 判断key是否存在
         *
         * @param key 键
         * @return true 存在 false不存在
         */
        public boolean hasKey(String key) {
            try {
                return redisTemplate.hasKey(key);
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 删除缓存
         *
         * @param key 可以传一个值 或多个
         */
        @SuppressWarnings("unchecked")
        public void del(String... key) {
            if (key != null && key.length > 0) {
                if (key.length == 1) {
                    redisTemplate.delete(key[0]);
                } else {
                    redisTemplate.delete(CollectionUtils.arrayToList(key));
                }
            }
        }
    
        //============================String=============================
    
        /**
         * 普通缓存获取
         *
         * @param key 键
         * @return 值
         */
        public Object get(String key) {
            return key == null ? null : redisTemplate.opsForValue().get(key);
        }
    
        /**
         * 普通缓存放入
         *
         * @param key   键
         * @param value 值
         * @return true成功 false失败
         */
        public boolean set(String key, Object value) {
            try {
                redisTemplate.opsForValue().set(key, value);
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
    
        }
    
        /**
         * 普通缓存放入并设置时间
         *
         * @param key   键
         * @param value 值
         * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
         * @return true成功 false 失败
         */
        public boolean set(String key, Object value, long time) {
            try {
                if (time > 0) {
                    redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
                } else {
                    set(key, value);
                }
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 递增
         *
         * @param key 键
         * @param delta  要增加几(大于0)
         * @return
         */
        public long incr(String key, long delta) {
            if (delta < 0) {
                throw new RuntimeException("递增因子必须大于0");
            }
            return redisTemplate.opsForValue().increment(key, delta);
        }
    
        /**
         * 递减
         *
         * @param key 键
         * @param delta  要减少几(小于0)
         * @return
         */
        public long decr(String key, long delta) {
            if (delta < 0) {
                throw new RuntimeException("递减因子必须大于0");
            }
            return redisTemplate.opsForValue().increment(key, -delta);
        }
    
        //================================Map=================================
    
        /**
         * HashGet
         *
         * @param key  键 不能为null
         * @param item 项 不能为null
         * @return 值
         */
        public Object hget(String key, String item) {
            return redisTemplate.opsForHash().get(key, item);
        }
    
        /**
         * 获取hashKey对应的所有键值
         *
         * @param key 键
         * @return 对应的多个键值
         */
        public Map<Object, Object> hmget(String key) {
            return redisTemplate.opsForHash().entries(key);
        }
    
        /**
         * HashSet
         *
         * @param key 键
         * @param map 对应多个键值
         * @return true 成功 false 失败
         */
        public boolean hmset(String key, Map<String, Object> map) {
            try {
                redisTemplate.opsForHash().putAll(key, map);
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * HashSet 并设置时间
         *
         * @param key  键
         * @param map  对应多个键值
         * @param time 时间(秒)
         * @return true成功 false失败
         */
        public boolean hmset(String key, Map<String, Object> map, long time) {
            try {
                redisTemplate.opsForHash().putAll(key, map);
                if (time > 0) {
                    expire(key, time);
                }
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 向一张hash表中放入数据,如果不存在将创建
         *
         * @param key   键
         * @param item  项
         * @param value 值
         * @return true 成功 false失败
         */
        public boolean hset(String key, String item, Object value) {
            try {
                redisTemplate.opsForHash().put(key, item, value);
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 向一张hash表中放入数据,如果不存在将创建
         *
         * @param key   键
         * @param item  项
         * @param value 值
         * @param time  时间(秒)  注意:如果已存在的hash表有时间,这里将会替换原有的时间
         * @return true 成功 false失败
         */
        public boolean hset(String key, String item, Object value, long time) {
            try {
                redisTemplate.opsForHash().put(key, item, value);
                if (time > 0) {
                    expire(key, time);
                }
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 删除hash表中的值
         *
         * @param key  键 不能为null
         * @param item 项 可以使多个 不能为null
         */
        public void hdel(String key, Object... item) {
            redisTemplate.opsForHash().delete(key, item);
        }
    
        /**
         * 判断hash表中是否有该项的值
         *
         * @param key  键 不能为null
         * @param item 项 不能为null
         * @return true 存在 false不存在
         */
        public boolean hHasKey(String key, String item) {
            return redisTemplate.opsForHash().hasKey(key, item);
        }
    
        /**
         * hash递增 如果不存在,就会创建一个 并把新增后的值返回
         *
         * @param key  键
         * @param item 项
         * @param by   要增加几(大于0)
         * @return
         */
        public double hincr(String key, String item, double by) {
            return redisTemplate.opsForHash().increment(key, item, by);
        }
    
        /**
         * hash递减
         *
         * @param key  键
         * @param item 项
         * @param by   要减少记(小于0)
         * @return
         */
        public double hdecr(String key, String item, double by) {
            return redisTemplate.opsForHash().increment(key, item, -by);
        }
    
        //============================set=============================
    
        /**
         * 根据key获取Set中的所有值
         *
         * @param key 键
         * @return
         */
        public Set<Object> sGet(String key) {
            try {
                return redisTemplate.opsForSet().members(key);
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
    
        /**
         * 根据value从一个set中查询,是否存在
         *
         * @param key   键
         * @param value 值
         * @return true 存在 false不存在
         */
        public boolean sHasKey(String key, Object value) {
            try {
                return redisTemplate.opsForSet().isMember(key, value);
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 将数据放入set缓存
         *
         * @param key    键
         * @param values 值 可以是多个
         * @return 成功个数
         */
        public long sSet(String key, Object... values) {
            try {
                return redisTemplate.opsForSet().add(key, values);
            } catch (Exception e) {
                e.printStackTrace();
                return 0;
            }
        }
    
        /**
         * 将set数据放入缓存
         *
         * @param key    键
         * @param time   时间(秒)
         * @param values 值 可以是多个
         * @return 成功个数
         */
        public long sSetAndTime(String key, long time, Object... values) {
            try {
                Long count = redisTemplate.opsForSet().add(key, values);
                if (time > 0) expire(key, time);
                return count;
            } catch (Exception e) {
                e.printStackTrace();
                return 0;
            }
        }
    
        /**
         * 获取set缓存的长度
         *
         * @param key 键
         * @return
         */
        public long sGetSetSize(String key) {
            try {
                return redisTemplate.opsForSet().size(key);
            } catch (Exception e) {
                e.printStackTrace();
                return 0;
            }
        }
    
        /**
         * 移除值为value的
         *
         * @param key    键
         * @param values 值 可以是多个
         * @return 移除的个数
         */
        public long setRemove(String key, Object... values) {
            try {
                Long count = redisTemplate.opsForSet().remove(key, values);
                return count;
            } catch (Exception e) {
                e.printStackTrace();
                return 0;
            }
        }
        //===============================list=================================
    
        /**
         * 获取list缓存的内容
         *
         * @param key   键
         * @param start 开始
         * @param end   结束  0 到 -1代表所有值
         * @return
         */
        public List<Object> lGet(String key, long start, long end) {
            try {
                return redisTemplate.opsForList().range(key, start, end);
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
    
        /**
         * 获取list缓存的长度
         *
         * @param key 键
         * @return
         */
        public long lGetListSize(String key) {
            try {
                return redisTemplate.opsForList().size(key);
            } catch (Exception e) {
                e.printStackTrace();
                return 0;
            }
        }
    
        /**
         * 通过索引 获取list中的值
         *
         * @param key   键
         * @param index 索引  index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
         * @return
         */
        public Object lGetIndex(String key, long index) {
            try {
                return redisTemplate.opsForList().index(key, index);
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
    
        /**
         * 将list放入缓存
         *
         * @param key   键
         * @param value 值
         * @return
         */
        public boolean lSet(String key, Object value) {
            try {
                redisTemplate.opsForList().rightPush(key, value);
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 将list放入缓存
         *
         * @param key   键
         * @param value 值
         * @param time  时间(秒)
         * @return
         */
        public boolean lSet(String key, Object value, long time) {
            try {
                redisTemplate.opsForList().rightPush(key, value);
                if (time > 0) expire(key, time);
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 将list放入缓存
         *
         * @param key   键
         * @param value 值
         * @return
         */
        public boolean lSet(String key, List<Object> value) {
            try {
                redisTemplate.opsForList().rightPushAll(key, value);
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 将list放入缓存
         *
         * @param key   键
         * @param value 值
         * @param time  时间(秒)
         * @return
         */
        public boolean lSet(String key, List<Object> value, long time) {
            try {
                redisTemplate.opsForList().rightPushAll(key, value);
                if (time > 0) expire(key, time);
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 根据索引修改list中的某条数据
         *
         * @param key   键
         * @param index 索引
         * @param value 值
         * @return
         */
        public boolean lUpdateIndex(String key, long index, Object value) {
            try {
                redisTemplate.opsForList().set(key, index, value);
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    
        /**
         * 移除N个值为value
         *
         * @param key   键
         * @param count 移除多少个
         * @param value 值
         * @return 移除的个数
         */
        public long lRemove(String key, long count, Object value) {
            try {
                Long remove = redisTemplate.opsForList().remove(key, count, value);
                return remove;
            } catch (Exception e) {
                e.printStackTrace();
                return 0;
            }
        }
    
        //================有序集合 sort set===================
        /**
         * 有序set添加元素
         *
         * @param key
         * @param value
         * @param score
         * @return
         */
        public boolean zSet(String key, Object value, double score) {
            return redisTemplate.opsForZSet().add(key, value, score);
        }
    
        public long batchZSet(String key, Set<ZSetOperations.TypedTuple> typles) {
            return redisTemplate.opsForZSet().add(key, typles);
        }
    
        public void zIncrementScore(String key, Object value, long delta) {
            redisTemplate.opsForZSet().incrementScore(key, value, delta);
        }
    
        public void zUnionAndStore(String key, Collection otherKeys, String destKey) {
            redisTemplate.opsForZSet().unionAndStore(key, otherKeys, destKey);
        }
    
        /**
         * 获取zset数量
         * @param key
         * @param value
         * @return
         */
        public long getZsetScore(String key, Object value) {
            Double score = redisTemplate.opsForZSet().score(key, value);
            if(score==null){
                return 0;
            }else{
                return score.longValue();
            }
        }
    
        /**
         * 获取有序集 key 中成员 member 的排名 。
         * 其中有序集成员按 score 值递减 (从大到小) 排序。
         * @param key
         * @param start
         * @param end
         * @return
         */
        public Set<ZSetOperations.TypedTuple> getZSetRank(String key, long start, long end) {
            return redisTemplate.opsForZSet().reverseRangeWithScores(key, start, end);
        }
    
    }
    
    

    实现类的代码

    // 每周热评的方法
    @Override
    public void initweek() {
        //获取 7天的文章
        List<MPost> posts = this.list(new QueryWrapper<MPost>().ge("created", DateUtil.lastWeek())
                  .select("id", "title", "user_id", "comment_count", "view_count", "created")
          );// 获取到7天前的以及按照这几个查询,不需要全部查询
          // 初始化文章的总评论
          for (MPost post : posts) {
              // 设置 key
              String key = "day:rank:" + DateUtil.format(post.getCreated(), DatePattern.PURE_DATE_FORMAT);
              // 缓存进去的评论数量
              redisUtil.zSet(key, post.getId(), post.getCommentCount());
              //设置自动过期 7天过期
              long between = DateUtil.between(new Date(), post.getCreated(), DateUnit.DAY);
              long expireTime = (7 - between) * 24 * 60 * 60; // 有效 时间
    
              redisUtil.expire(key, expireTime);
              // 缓存文章的一些基本信息
              this.hashCachePost(post, expireTime);
          }
          // 做并集
          this.zunionAndStore();
    }
    
     /**
      * 文章每周评论数量并集操作
      **/
     private void zunionAndStore() {
         String destkey = "day:rank:" + DateUtil.format(new Date(), DatePattern.PURE_DATE_FORMAT);
         // 设置并集后的新的 key
         String newkey = "week:rank";
         ArrayList<String> otherKeys = new ArrayList<>();
         // 计算7天的
         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);
         }
         redisUtil.zUnionAndStore(destkey, otherKeys, newkey);
     }
    
     /**
      * 文章作者缓存
      **/
     private void hashCachePost(MPost post, long expireTime) {
         // 设置 key
         String key = "rank:post:" + post.getId();
         // 判断存在不存在
         boolean hasKey = redisUtil.hasKey(key);
         if (!hasKey) {
             // 就存到缓存里面
             redisUtil.hset(key, "post:id", post.getId(), expireTime);
             redisUtil.hset(key, "post:title", post.getTitle(), expireTime);
             redisUtil.hset(key, "post:commentCount", post.getCommentCount(), expireTime);
    
         }
     }
    }
    
    

    这样就可以把我们的命令行转换成代码的形式。就可以把我们的数据库的数据先存到缓存中去了。

    效果

    127.0.0.1:6379> keys *
    1) "rank:post:4"
    2) "week:rank"
    3) "day:rank:20210724"
    4) "rank:post:3"
    5) "rank:post:2"
    6) "day:rank:20210726"
    #查看我们并集完后的数据 id 为 3 的有 1条评论。
    127.0.0.1:6379> zrevrange week:rank 0 -1 withscores
    1) "3"
    2) "1"
    3) "2"
    4) "1"
    5) "4"
    6) "0"
    127.0.0.1:6379>
    
    

    数据库中id 为 3 的有 1条评论

    image.png image.png

    确实只有一条评论的

    前端展示出来

    这里的思路就比较简单了,把我们的数据从缓存中取出来就可以。用的freemarker可以自定义标签。我自定义了标签。

    Hosttemplate

    /**
     * 本周热议
     */
    @Component
    public class HotsTemplate extends TemplateDirective {
    
        @Autowired
        RedisUtil redisUtil;
    
        @Override
        public String getName() {
            return "hots";
        }
    
        @Override
        public void execute(DirectiveHandler handler) throws Exception {
    // 设置 key
            String key ="week:rank";
            Set<ZSetOperations.TypedTuple> zSetRank = redisUtil.getZSetRank(key, 0, 6);
            ArrayList<Map> maps = new ArrayList<>();
    
            // 便利
            for (ZSetOperations.TypedTuple typedTuple : zSetRank) {
                // 创建 Map
                HashMap<String, Object> map = new HashMap<>();
                Object post_id = typedTuple.getValue();
                String PostHashKey = "rank:post:" +post_id;
                map.put("id",post_id);
                map.put("title",redisUtil.hget(PostHashKey,"post:title"));
                map.put("commentCount",typedTuple.getScore());
                maps.add(map);
            }
            handler.put(RESULTS,maps).render();
        }
    }
    
    

    FreemarkerConfig把我们写的便签注入就可以使用我们自定义的标签了

    @Configuration
    public class FreemarkerConfig {
    
        @Autowired
        private freemarker.template.Configuration configuration;
        @Autowired
        PostsTemplate postsTemplate;
        @Autowired
        HotsTemplate hotsTemplate;
        @PostConstruct
        public void setUp() {
            configuration.setSharedVariable("timeAgo", new TimeAgoMethod());
            configuration.setSharedVariable("posts", postsTemplate);
            configuration.setSharedVariable("hosts", hotsTemplate);
        }
    
    }
    
    

    前端的页面获取

    image.png

    效果

    image.png

    相关文章

      网友评论

          本文标题:面试官:Redis 如何实现每周热评功能?说说思路!

          本文链接:https://www.haomeiwen.com/subject/zlnpzrtx.html