SpringBoot+redis定时任务统计浏览量

需求

在用户浏览博文时要实现对应博客浏览量的增加。

思路分析

我们只需要在每次用户浏览博客时更新对应的浏览数即可。

但是如果直接操作博客表的浏览量的话,在并发量大的情况下会出现什么问题呢?

如何去优化呢?

现在的操作流程是这样的:当前端/移动端通过后台对访问量去进行更新时,mysql现在是死锁的一个状态,那么如果是并发情况再来一个前端/移动端去访问mysql时它是死锁状态无法读取同一条数据的,是会受到影响的,那么我们需要处理这个问题,那么怎么做呢?。

通过redis可以处理这个问题,如果我们去更新数据库的话会影响其它用户的读,所以我们就可以不通过mysql去更新这个这个数据,通过一个中间的东西去更新这条数据,这个中间的东西就是redis。

那么我们的操作流程就可以改变了,当前端通过后端更新访问量时是通过redis递增浏览量,读取数据时也是用mysql这样就不会有死锁的问题了。

1.那么问题又来了,现在前端通过后端访问redis中redis中是没有数据的,那么应该怎么做呢?
可以在项目启动的时候去读取mysql中的数据去写入存入到redis当中,这样redis中也有数据了。
2.现在又有问题了,前端通过后端访问redis去递增浏览量(假如递增了很多浏览量),mysql是没有更新的还是原来的值的,然后reids关掉了或者出问题了导致递增的浏览量全没了还是mysql原来的浏览量那么又该怎么办呢?
所以这里就用到同步问题了,就是把redis的数据同步到mysql中。但是这个同步是和前面的前端通过后端接口每次请求时都更新一次不同,这样是非常频繁的,现在用到redis去同步mysql时就不用如此频繁了,如果现在本次浏览量更新到mysql后但是在下一次redis更新前就关掉了这是没有关系的,比如10分钟、20分钟、一小时更新一次,这样即使损失一部分浏览量也是很少的。
3.还有一个问题就是,以前的接口读取访问量是通过mysql直接读取浏览量的,现在要更改为从redis中获取浏览量,这样的好处也是为了实时更新。

也就有下面几步:
①在应用启动时把博客的浏览量存储到redis中 其中id作为key,浏览量作为值存入到redis中
②更新浏览量时去更新redis中的数据

铺垫知识

3.18.3.1 CommandLineRunner实现项目启动时预处理

如果希望在SpringBoot应用启动时进行一些初始化操作可以选择使用CommandLineRunner来进行处理。

我们只需要实现CommandLineRunner接口,并且把对应的bean注入容器。把相关初始化的代码重新到需要重新的方法中。

这样就会在应用启动的时候执行对应的代码。

@Component
public class TestRunner implements CommandLineRunner {
    @Override
    public void run(String... args) throws Exception {
        System.out.println("程序初始化");
    }
}

定时任务

定时任务的实现方式有很多,比如XXL-Job等。但是其实核心功能和概念都是类似的,很多情况下只是调用的API不同而已。
这里就先用SpringBoot为我们提供的定时任务的API来实现一个简单的定时任务,让大家先对定时任务里面的一些核心概念有个大致的了解。

实现步骤

① 使用@EnableScheduling注解开启定时任务功能

我们可以在配置类上加上@EnableScheduling

@SpringBootApplication
@MapperScan("com.sangeng.mapper")
@EnableScheduling
public class SanGengBlogApplication {
    public static void main(String[] args) {
        SpringApplication.run(SanGengBlogApplication.class,args);
    }
}

② 确定定时任务执行代码,并配置任务执行时间

使用@Scheduled注解标识需要定时执行的代码。注解的cron属性相当于是任务的执行时间。目前可以使用 0/5 * * * * ? 进行测试,代表从0秒开始,每隔5秒执行一次。

注意:对应的bean要注入容器,否则不会生效。

@Component
public class TestJob {

    @Scheduled(cron = "0/5 * * * * ?")
    public void testJob(){
        //要执行的代码
        System.out.println("定时任务执行了");
    }
}
cron 表达式语法

​ cron表达式是用来设置定时任务执行时间的表达式。

​ 很多情况下我们可以用 : 在线Cron表达式生成器 来帮助我们理解cron表达式和书写cron表达式。

​ 但是我们还是有需要学习对应的Cron语法的,这样可以更有利于我们书写Cron表达式。

如上我们用到的 0/5 * * * * ? *,cron表达式由七部分组成,中间由空格分隔,这七部分从左往右依次是:

秒(059),分钟(059),小时(0~23),日期(1-月最后一天),月份(1-12),星期几(1-7,1表示星期日),年份(一般该项不设置,直接忽略掉,即可为空值)

通用特殊字符:, - * / (可以在任意部分使用)

*

星号表示任意值,例如:

* * * * * ?

表示 “ 每年每月每天每时每分每秒 ” 。

,   

可以用来定义列表,例如 :

1,2,3 * * * * ?

表示 “ 每年每月每天每时每分的每个第1秒,第2秒,第3秒 ” 。

-

定义范围,例如:

1-3 * * * * ?

表示 “ 每年每月每天每时每分的第1秒至第3秒 ”。

/

每隔多少,例如

5/10 * * * * ?

表示 “ 每年每月每天每时每分,从第5秒开始,每10秒一次 ” 。即 “ / ” 的左侧是开始值,右侧是间隔。如果是从 “ 0 ” 开始的话,也可以简写成 “ /10 ”

日期部分还可允许特殊字符: ? L W

星期部分还可允许的特殊字符: ? L #

?

只可用在日期和星期部分。表示没有具体的值,使用?要注意冲突。日期和星期两个部分如果其中一个部分设置了值,则另一个必须设置为 “ ? ”。

例如:

0\* * * 2 * ?
 和
0\* * * ? * 2

同时使用?和同时不使用?都是不对的

例如下面写法就是错的

* * * 2 * 2
 和
* * * ? * ?

W

只能用在日期中,表示当月中最接近某天的工作日

0 0 0 31W * ?

表示最接近31号的工作日,如果31号是星期六,则表示30号,即星期五,如果31号是星期天,则表示29号,即星期五。如果31号是星期三,则表示31号本身,即星期三。

L

表示最后(Last),只能用在日期和星期中

在日期中表示每月最后一天,在一月份中表示31号,在六月份中表示30号

也可以表示每月倒是第N天。例如: L-2表示每个月的倒数第2天

0 0 0 LW * ?
LW可以连起来用,表示每月最后一个工作日,即每月最后一个星期五

在星期中表示7即星期六

0 0 0 ? * L
表示每个星期六
0 0 0 ? * 6L
若前面有其他值的话,则表示最后一个星期几,即每月的最后一个星期五

代码实现

①在应用启动时把博客的浏览量存储到redis中

实现CommandLineRunner接口,在应用启动时初始化缓存。

# 

只能用在星期中,表示第几个星期几

0 0 0 ? * 6#3
表示每个月的第三个星期五。
@Component
public class ViewCountRunner implements CommandLineRunner {

    @Autowired
    private ArticleMapper articleMapper;

    @Autowired
    private RedisCache redisCache;

    @Override
    public void run(String... args) throws Exception {
        //查询博客信息  id  viewCount
        List<Article> articles = articleMapper.selectList(null);
        Map<String, Integer> viewCountMap = articles.stream()
                .collect(Collectors.toMap(article -> article.getId().toString(), article -> {
                    return article.getViewCount().intValue();//
                }));
        //存储到redis中
        redisCache.setCacheMap("article:viewCount",viewCountMap);
    }
}

②更新浏览量时去更新redsi中的数据

RedisCache增加方法

    public void incrementCacheMapValue(String key,String hKey,long v){
        redisTemplate.boundHashOps(key).increment(hKey, v);
    }

ArticleController中增加方法更新阅读数

    @PutMapping("/updateViewCount/{id}")
    public ResponseResult updateViewCount(@PathVariable("id") Long id){
        return articleService.updateViewCount(id);
    }

ArticleService中增加方法

ResponseResult updateViewCount(Long id);

ArticleServiceImpl中实现方法

    @Override
    public ResponseResult updateViewCount(Long id) {
        //更新redis中对应 id的浏览量
        redisCache.incrementCacheMapValue("article:viewCount",id.toString(),1);
        return ResponseResult.okResult();
    }
③定时任务每隔10分钟把Redis中的浏览量更新到数据库中

Article中增加构造方法

   public Article(Long id, long viewCount) {
        this.id = id;
        this.viewCount = viewCount;
    }
@Component
public class UpdateViewCountJob {

    @Autowired
    private RedisCache redisCache;

    @Autowired
    private ArticleService articleService;

    @Scheduled(cron = "0/5 * * * * ?")
    public void updateViewCount(){
        //获取redis中的浏览量
        Map<String, Integer> viewCountMap = redisCache.getCacheMap("article:viewCount");

        List<Article> articles = viewCountMap.entrySet()
                .stream()
                .map(entry -> new Article(Long.valueOf(entry.getKey()), entry.getValue().longValue()))
                .collect(Collectors.toList());
        //更新到数据库中
        articleService.updateBatchById(articles);

    }
}

④读取文章浏览量时从redis读取

    @Override
    public ResponseResult getArticleDetail(Long id) {
        //根据id查询文章
        Article article = getById(id);
        //从redis中获取viewCount
        Integer viewCount = redisCache.getCacheMapValue("article:viewCount", id.toString());
        article.setViewCount(viewCount.longValue());
        //转换成VO
        ArticleDetailVo articleDetailVo = BeanCopyUtils.copyBean(article, ArticleDetailVo.class);
        //根据分类id查询分类名
        Long categoryId = articleDetailVo.getCategoryId();
        Category category = categoryService.getById(categoryId);
        if(category!=null){
            articleDetailVo.setCategoryName(category.getName());
        }
        //封装响应返回
        return ResponseResult.okResult(articleDetailVo);
    }

posted @ 2022-08-12 15:18  长情c  阅读(1305)  评论(0)    收藏  举报