最近开发个人博客,想统计单网页浏览量,一开始是想把浏览量记录在数据库,后来想想每次点击网页就要去做数据库更新操作,实际项目是不会允许的,还是老老实实用redis来处理吧!
0. 需求
- 网页列表显示时按评论数倒序排列
- 进入单页页面时,该页面浏览量自动+1
- 系统启动时,将数据库中的浏览量,评论数,点赞数添加到redis数据库中
- 系统关闭时,自动将redis中数据更新到mysql数据库中
之所以选择在启动和关闭时进行redis与mysql数据交换,也是为了防止高并发。
1. 效果
数据按照“评论数”倒序排列,点击相关标题进入单页面,该页面浏览量自动+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
- 数据表:
- 对应的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;
}
- 查询类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;
}
- 启动关闭时操作,主要定义监听类:
@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+" 更新完毕");
});
}
}
4. 利用aop拦截单页访问请求
- 引入aop相关依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
- 单页访问的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);
}
- 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记录值替换 => 最后根据浏览量倒序排列。
/**
* 调用分页插件完成分页
* @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处理。