springboot - redis记录并统计网页浏览量

最近开发个人博客,想统计单网页浏览量,一开始是想把浏览量记录在数据库,后来想想每次点击网页就要去做数据库更新操作,实际项目是不会允许的,还是老老实实用redis来处理吧!

0. 需求

  • 网页列表显示时按评论数倒序排列
  • 进入单页页面时,该页面浏览量自动+1
  • 系统启动时,将数据库中的浏览量,评论数,点赞数添加到redis数据库中
  • 系统关闭时,自动将redis中数据更新到mysql数据库中

之所以选择在启动和关闭时进行redis与mysql数据交换,也是为了防止高并发。

1. 效果

《springboot - redis记录并统计网页浏览量》
数据按照“评论数”倒序排列,点击相关标题进入单页面,该页面浏览量自动+1。

2. redis配置

引入redis相关依赖“

   <!--操作spring-redis-->
     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-data-redis</artifactId>
     </dependency>

2.1 spring配置

spring:
  redis:
    host: 127.0.0.1  //主机
    port: 6379       //端口

2.2 RedisTemplate

@Configuration
public class RedisConfig { 

    //编写我们自己的template
    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
            throws UnknownHostException { 

        //我们为了自己开发方便,一般使用<String,Object>
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);

        //Json序列化配置
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
        ObjectMapper om=new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);

        //String的序列化
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        //key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        //hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        //value的序列化采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        //hash的value也采用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);

        template.afterPropertiesSet();

        return template;
    }

}

2.3 RedisUtil

本案例中主要用到的方法如下:

    /** * zet增加操作 * @param key * @param value 属性值 * @param map 具体分数 * @return */
    public Boolean zsAdd(String key, String value, HashMap<String, Object> map){ 
        try { 
// redisTemplate.opsForZSet().add("viewNum", "h1", Double.valueOf(h1.get("viewNum").toString()));

            redisTemplate.opsForZSet().add(key, value, Double.valueOf(map.get(key).toString()));

            return true;

        } catch (Exception e) { 

            e.printStackTrace();
            return false;
        }

    }

    /** * zset给某个key某个属性增值操作 * @param key * @param value 属性值 * @param delta 增加值 * @return */
    public Boolean zsIncr(String key, String value, Integer delta){ 
        try { 
            redisTemplate.opsForZSet().incrementScore(key, value, delta);
            return true;
        } catch (Exception e) { 
            e.printStackTrace();
            return false;
        }

    }

    /** * zset逆向排序 * @param key * @return */
    public Set<Object>  zsReverseRange(String key){ 
        Set viewNum = redisTemplate.opsForZSet().reverseRange(key,0,-1);

        return viewNum;

    }

    /** * zscore 返回属性值 * @param key key值 * @param value 属性值 * @return */
    public Double zscore(String key,String value){ 
        Double score = redisTemplate.opsForZSet().score(key, value);
        return score;
    }

3. 启动时将数据写入redis,关闭时从redis写入到mysql

  1. 数据表:
    《springboot - redis记录并统计网页浏览量》
  2. 对应的aritcle类:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Article implements Serializable { 


    private Long articleId;
    private Long userId=1l ;
    private String title;
    private String name;
    private Integer viewNum=0 ;
    private Integer commentNum=0 ;
    private long categoryId ;
    private Timestamp createTime;

    //添加点赞数
    private Integer likeNum=0;

    //添加乐观锁
    private Integer version;
}
  1. 查询类ArticleQuery:
@Data
public class ArticleQuery extends Article { 

    //排序字段
    public String sortView;
    public String sort;

    public String getSort(){ 
        if(sortView!=null){ 
            this.sort=StringTool.humpToLine(this.sortView); //将sort驼峰命名转换为下划线命名
        }
        return this.sort;
    }


    //排序方向
    public String direction;

    //页面大小
    public Integer pageSize;

    //页数
    public Integer pageNum;


}
  1. 启动关闭时操作,主要定义监听类:
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class ListenHandler { 

    @Autowired
    private ArticleService articleService;

    @Autowired
    private RedisUtil redisUtil;

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    public ListenHandler(){ 
        this.logger.info("开始数据初始化");
    }

    @PostConstruct
    public void init() throws Exception { 
        this.logger.info("数据库 初始化开始");
        //将数据库中的数据写入redis
        List<Article> articleList = articleService.queryAll();
        articleList.forEach(article -> { 
            //将浏览量写入redis
            //分别有浏览量,点赞数,评论数
            HashMap<String, Object> h1 = new HashMap<>();
            h1.put("viewNum",article.getViewNum());
            h1.put("likeNum",article.getLikeNum());
            h1.put("commentNum",article.getCommentNum());
            redisUtil.zsAdd("commentNum",article.getArticleId().toString(),h1);
            redisUtil.zsAdd("viewNum",article.getArticleId().toString(),h1);
            redisUtil.zsAdd("likeNum",article.getArticleId().toString(),h1);
        });

        this.logger.info("已写入redis");

    }
    //关闭时操作
    @PreDestroy
    public void afterDestroy(){ 
        System.out.println("关闭====================================");
        //将redis中的数据一次性写入数据库
        Set<DefaultTypedTuple> viewNum = redisUtil.zsReverseRangeWithScores("viewNum");
        Set<DefaultTypedTuple> commentNum = redisUtil.zsReverseRangeWithScores("commentNum");
        Set<DefaultTypedTuple> likeNum = redisUtil.zsReverseRangeWithScores("likeNum");
        this.writeNum(viewNum,"viewNum");
        this.writeNum(commentNum,"commentNum");
        this.writeNum(likeNum,"likeNum");

        this.logger.info("数据库拿出到redis完毕");
        System.out.println("系统关闭===========reids->数据库更新完毕=================");
    }


    public void writeNum(Set<DefaultTypedTuple> set,String fieldName){ 
        set.forEach(item->{ 
            DefaultTypedTuple ii= (DefaultTypedTuple)item;
            Long id = Long.valueOf((String)ii.getValue());
            Integer num = ii.getScore().intValue();

            Article article = articleService.queryById(id);
            if(fieldName.equals("viewNum")){ 
                article.setViewNum(num);
            }
            else if(fieldName.equals("commentNum")){ 
                article.setCommentNum(num);
            }
            else{ 
                article.setLikeNum(num);
            }

            //更新数据库
            articleService.updateArticle(article);
            this.logger.info(fieldName+" 更新完毕");

        });

    }
}

ArticleService相关定义见https://blog.csdn.net/ws6afa88/article/details/108739365

4. 利用aop拦截单页访问请求

  1. 引入aop相关依赖:
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
  1. 单页访问的controller层函数:
    @GetMapping("api/queryById/{id}")
    public ResponseEntity<Object> queryById(@PathVariable("id")  long id){ 
        Article article = articleService.queryById(id);
        if(article==null){ 
            throw new BadRequestException("此id不存在");
        }

        return new ResponseEntity<>(article, HttpStatus.OK);
    }
  1. aop拦截:
//指定为切面类
@Aspect
//将该类放入Spring容器中
@Component
public class MyAspect { 

    @Autowired
    ArticleService articleService;

    @Autowired
    private RedisUtil redisUtil;

    //定义一个名为"myPointCut()"的切面,位置就在queryById()这个方法中
    @Pointcut("execution(public * com.xinxin.controller.ArticleController.queryById(..))")
    public void myPointCut(){ }

    //在这个方法执行后
    @After("myPointCut()")
    public void doAfter(JoinPoint joinPoint) throws Throwable { 
        Object[] objs=joinPoint.getArgs();
        Long id=(Long) objs[0];
        //根据id更新浏览量
        redisUtil.zsIncr("viewNum",id.toString(),1);
    }
}

5.分页操作中利用redis中记录的值来排序

思路是先从数据表中分页查询所需要的行 => 再将行中的浏览量等参数用redis记录值替换 => 最后根据浏览量倒序排列。其中关于分页的思路与源代码见https://blog.csdn.net/ws6afa88/article/details/108928401

    /** * 调用分页插件完成分页 * @param aq * @return */
    private PageInfo<Article> getPageInfo(ArticleQuery aq) { 
        PageHelper.startPage(aq.getPageNum(), aq.getPageSize());
        //从redis中按sort倒序查询得到ids
        Set<Object> set = redisUtil.zsReverseRange(aq.getSortView());
        List ids=new ArrayList<Long>();
        set.forEach(item->{ 
            ids.add(Long.valueOf(item.toString()));
        });

        List<Article> articles = articleDao.queryByCondition(aq);

        //将artcile中的Num数据换为redis中的
        articles.forEach(article -> { 
            String id = article.getArticleId().toString();
            article.setViewNum(redisUtil.zscore("viewNum",id).intValue());
            article.setCommentNum(redisUtil.zscore("commentNum",id).intValue());
            article.setLikeNum(redisUtil.zscore("likeNum",id).intValue());

        });

        //按照sort指定值排倒序,这里用到了Comparator匿名内部类来定义排序规则
        Collections.sort(articles, new Comparator<Article>() { 
            @Override
            public int compare(Article o1, Article o2) { 

                int diff=-3;
                if(aq.getSort().equals("comment_num")){ 
                     diff = o2.getCommentNum() - o1.getCommentNum();
                }
                else if(aq.getSort().equals("view_num")){ 
                     diff = o2.getViewNum() - o1.getViewNum();

                }else{ 
                     diff = o2.getLikeNum() - o1.getLikeNum();
                }

                if (diff > 0) { 
                    return 1;
                }else if (diff < 0) { 
                    return -1;
                }
                    return 0; //相等为0
                }
            });

        return new PageInfo<Article>(articles);
    }
}

6. 总结

其实可以用定时器来做redis与mysql的数据交互,下一篇博文来补充吧!redis记录浏览量并自动排序,其实不需要用Zset类型来处理,因为List定义好Comparator匿名内部类就可以排序,这也是一开始没想好,后面慢慢改回来。Redis计算能力还是相当强,以后update操作尽量都放在Redis处理。

    原文作者:Lydia的IT世界是橙色的
    原文地址: https://blog.csdn.net/ws6afa88/article/details/108989549
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞