当前位置: 首页 > article >正文

异步任务与定时任务

一、异步任务

        基于TaskExecutionAutoConfiguration配置类中,注册的ThreadPoolTaskExecutor线程池对象进行异步任务执行。

(一)手动执行异步任务

在yml中配置线程池参数

spring:            
  task:
    execution:
      pool:
        core-size: 5  # 核心线程数
        max-size: 20  # 最大线程数
        queue-capacity: 1000  # 线程池使用的阻塞队列的最大容量
    scheduling:
      pool:
        size: 10  # 配置了调度任务的线程池:线程数为10 (: 10)

代码示例:

@Resource
private ThreadPoolTaskExecutor taskExecutor;

@Override
public WebUser doUnameLogin(String username, String password) {
    // 根据用户名 密码 查询用户信息
    WebUser webUser = webUserService.getByUname(username);
    // 判断用户是否存在
    if (webUser == null) {
        throw new JavasmException(JavasmExceptionEnum.UserNotExist);
    }
    // 判断密码
    if (!password.equals(webUser.getPassword())) {
        throw new JavasmException(JavasmExceptionEnum.PasswordError);
    }
    /*new Thread(() -> {        
    }).start();*/

    // 使用线程池异步执行,避免阻塞主线程,而不是使用new Thread
    taskExecutor.execute(()->{
        // 增加需求:每天,每个用户只记录一次登录信息
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd");
        String date = simpleDateFormat.format(new Date());
        // 从redis中查询 当前用户,是否有登录记录
        String loginLogKey = String.format(RedisKeys.Login_Log_Key, date, webUser.getUid());
        Object o = redisTemplate.opsForValue().get(loginLogKey);
        if (o == null) { // 用户当天没有登录记录,存入redis和数据库中
            // 登录时,需要一起查询webUserInfo的内容
            // 记录登录信息
            WebUserLoginLog webUserLoginLog = new WebUserLoginLog();
            webUserLoginLog.setUid(webUser.getUid());
            webUserLoginLogService.save(webUserLoginLog);
            redisTemplate.opsForValue().set(loginLogKey, webUser.getUid(), 1, TimeUnit.DAYS);
        }
    });
    // 登录成功 返回用户信息
    return webUser;
}
@Override
@Transactional
public void doRegister(WebUser webUser) {
    // TODO:查询邮箱是否已经存在
    // 添加web_user表  web_user_info表
    webUserService.save(webUser);
    if (webUser != null && webUser.getWebUserInfo() != null) {
        webUser.getWebUserInfo().setUid(webUser.getUid());
        // 随机分配头像
        String baseUrl = "http://cd.ray-live.cn/imgs/headpic/pic_%s.jpg";
        // 随机数:ThreadLocalRandom.current线程安全,顾前不顾后
        int index = ThreadLocalRandom.current().nextInt(0, 70);
        webUser.getWebUserInfo().setHeadPic(String.format(baseUrl, index));
        webUserInfoService.save(webUser.getWebUserInfo());
    }
    WebUser newWebUser = webUser.clone();
    // 这里使用克隆的原因:上面没有给webUserInfo赋值。下面的代码会用到webUserInfo,我们不需要webUserInfo中有值
    // 先执行上面的代码,然后才会执行下面的代码,不存在线程调用异常的问题
    // 存入redis

    // 由于往Redis中添加数据,不属于注册主流程,要放到子线程中
    /*new Thread(() -> {
    }).start();*/

    // 使用线程池异步执行,避免阻塞主线程
    taskExecutor.execute(()->{
        // 配置的是数据库添加的默认值,此时的 webUser.getUserInfo() 是没有其他默认属性的
        // 想获取全部的数据,存入Redis,需要重新查询
        Integer uid = newWebUser.getUid();
        WebUserInfo userInfo = webUserInfoService.getById(uid);
        newWebUser.setWebUserInfo(userInfo);
        String unamekey = String.format(RedisKeys.User_Uname, newWebUser.getUsername());
        // 因为RedisTemplate<String, Object> ,所以可以传入webUser
        redisTemplate.opsForValue().set(unamekey, newWebUser);
        String uidKey = String.format(RedisKeys.User_Uid, newWebUser.getUid());
        redisTemplate.opsForValue().set(uidKey, newWebUser);
    });
}

(二)基于异步注解

启动异步注解识别

@Configuration
@MapperScan("com.javaplay.playPal.*.dao")
@EnableAsync // 开启异步注解
public class ServerConfig {
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

在需要异步执行的方法添加@Async注解

@Async
public void test(String str){
    log.info(str);
}

不建议直接在本类中使用,因为异步代码散乱到项目各个类中,不易后期维护,且必须跨类才支持异步注解使用

二、定时任务

        定时任务是基于TaskSchedulingAutoConfiguration配置类中,注册的ThreadPoolTaskScheduler任务调度线程池对象。不论是基于注解还是基于SchedulingConfigurer进行定时任务实现,都需要首先在配置类中启用定时任务。  

(一)启动定时任务 

@Configuration
@MapperScan("com.javaplay.playPal.*.dao")
@EnableAsync // 开启异步注解
@EnableScheduling // 开启定时任务注解
public class ServerConfig {
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

是否需要同时使用异步和定时?

  • 如果定时任务不需要异步执行,仅需使用@EnableScheduling和@Scheduled注解即可满足需求。
  • 如果希望定时任务能够异步执行,避免阻塞主线程(例如,避免一个定时任务的执行阻塞另一个定时任务),那么除了@EnableScheduling外,还需要添加@EnableAsync。这样,可以将@Async注解应用到定时任务的方法上,使其异步执行。 

(二)固定的定时任务

在编码阶段,已经固定了定时的逻辑,不能在不停止服务器的情况下更改逻辑。

例如,每天凌晨1点执行某个查询任务,如果想要改为每天凌晨2点,就必须停止服务器,更改定时任务规则,再重新启动服务器。

@Component
@Slf4j
public class TestTask {
    // 希望多久/什么频率, 执行一次/多次 当前的方法
    // 从第0秒开始,每隔5秒执行1次
    // cron="秒 分 小时 日期 月份 星期 年"
    // 星期和日期互斥,不能同时设置,必须有1个是?  年是可以省略的
    @Scheduled(cron = "0/5 * * * * ?")
    @Async
    public void f1() {
        log.info("---------------测试f1 ------每隔5秒执行1次");
    }

    // 从第10秒开始,每秒执行1次,第20秒的时候终止
    @Scheduled(cron = "10-20 * * * * ?")
    @Async // 可选,是否加异步,可自定义
    public void f2() {
        log.info("===========f2--从第10秒开始,每秒执行1次,第20秒的时候终止");
    }
}

#经典案例: 
“30 * * * * ?” 每分钟第30秒触发任务
“30 10 * * * ?” 每小时的10分30秒触发任务 
“30 10 1 * * ?” 每天1点10分30秒触发任务 
“30 10 1 20 * ?” 每月20号1点10分30秒触发任务 
“30 10 1 20 10 ? *” 每年10月20号1点10分30秒触发任务 
“30 10 1 20 10 ? 2011” 2011年10月20号1点10分30秒触发任务 
“30 10 1 ? 10 * 2011” 2011年10月每天1点10分30秒触发任务 
“30 10 1 ? 10 SUN 2011” 2011年10月每周日1点10分30秒触发任务 
“15,30,45 * * * * ?” 每分钟的第15秒,30秒,45秒时触发任务 
“15-45 * * * * ?” 15到45秒内,每秒都触发任务 
“15/5 * * * * ?” 每分钟的每15秒开始触发,每隔5秒触发一次 
“15-30/5 * * * * ?” 每分钟的15秒到30秒之间开始触发,每隔5秒触发一次 
“0 0/3 * * * ?” 每小时的第0分0秒开始,每三分钟触发一次 
“0 15 10 ? * MON-FRI” 星期一到星期五的10点15分0秒触发任务 
“0 15 10 L * ?” 每个月最后一天的10点15分0秒触发任务 
“0 15 10 LW * ?” 每个月最后一个工作日的10点15分0秒触发任务 
“0 15 10 ? * 5L” 每个月最后一个星期四的10点15分0秒触发任务 
“0 15 10 ? * 5#3”每个月第三周的星期四的10点15分0秒触发任务
@Resource
NewsService newsService;

// 同步新闻列表
@Scheduled(cron = "0 0 0/1 * * ?")
@Async
public void f3() {
    log.info("每天00:00:00开始同步新闻列表,每隔1小时执行1次");
    newsService.syncNews();
}

(三)可变的定时任务

package com.javaplay.playPal.task.runnable;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
public class TestTask implements Runnable {
    @Override
    public void run() {
        log.info("定时任务,执行了");
    }
}
@Component
@Slf4j
public class NewsTask implements Runnable{
    @Resource
    NewsService newsService;
    @Override
    public void run() {
        log.info("开始同步新闻");
        newsService.syncNews();
    }
}
create table sys_task
(
    id     int auto_increment primary key,
    name   varchar(255)  null comment '任务名称',
    clazz  varchar(255)  null comment '执行任务的类',
    cron   varchar(255)  null comment '定时任务表达式',
    status int default 0 null comment '状态 0关闭 1开启',
    ctime  datetime      null comment '创建时间'
);

INSERT INTO testdb.sys_task (id, name, clazz, cron, status, ctime)
VALUES (1, '测试定时任务', 'com.javaplay.playPal.task.runnable.TestTask', '0/5 * * * * ?', 0, '2025-01-17 19:50:48');
INSERT INTO testdb.sys_task (id, name, clazz, cron, status, ctime)
VALUES (2, '新闻同步', 'com.javaplay.playPal.task.runnable.NewsTask', '0/10 * * * * ?', 0, '2025-01-17 20:39:14');
@Component
@Slf4j
public class JavaTestSchedulingConfigurer implements SchedulingConfigurer {
    private ScheduledTaskRegistrar scheduledTaskRegistrar;

    private Map<Integer, ScheduledTask> map = new ConcurrentHashMap<>();

    @Resource
    ApplicationContext applicationContext;

    @Override
    public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
        // 项目启动的时候,就已经调用了这个方法,并且是在启动成功之前调用的
        // 考虑到有可能有多个定时任务,所以,要创建一个线程池,专门用来存放定时任务
        // 创建一个包含10个线程的调度线程池executorService。
        ScheduledExecutorService executorService = Executors.newScheduledThreadPool(10);
        // 使用该线程池创建一个ConcurrentTaskExecutor对象taskExecutor,用于执行定时任务。
        ConcurrentTaskScheduler taskScheduler = new ConcurrentTaskScheduler(executorService);
        // 开启注册 定时任务对象
        scheduledTaskRegistrar.setScheduler(taskScheduler);
        // 放到全局变量,这样其他方法,就可以调用/使用 参数了
        this.scheduledTaskRegistrar = scheduledTaskRegistrar;
    }

    public boolean regTask(SysTask task) {
        if (task == null) {
            return false;
        }
        // 任务id
        Integer id = task.getId();
        // 执行任务的类
        String clazz = task.getClazz();
        // 表达式
        String cron = task.getCron();
        // new对象
        try {
            Class<?> aClass = Class.forName(clazz);
            // Object o = aClass.getConstructor().newInstance();
            Object o = applicationContext.getBean(aClass);
            Runnable runnable = (Runnable) o;
            // 执行任务对象
            CronTask cronTask = new CronTask(runnable, cron);
            // 任务开始
            ScheduledTask scheduledTask = this.scheduledTaskRegistrar.scheduleCronTask(cronTask);
            // 将已经开始的任务对象存入全局,等待停止
            map.put(id, scheduledTask);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
        return true;
    }

    public void stop(Integer id) {
        ScheduledTask scheduledTask = map.get(id);
        if (scheduledTask != null) {
            // 停止定时任务
            scheduledTask.cancel();
            map.remove(id);
        }
    }
}
@RestController
@RequestMapping("/task")
public class SysTaskController {
    /**
     * 服务对象
     */
    @Resource
    private SysTaskService sysTaskService;

    @GetMapping("/start/{id}")
    public R start(@PathVariable Integer id) {
        sysTaskService.startTask(id);
        return R.ok();
    }

    @GetMapping("/stop/{id}")
    public R stop(@PathVariable Integer id) {
        sysTaskService.stopTask(id);
        return R.ok();
    }
}
@Service("sysTaskService")
public class SysTaskServiceImpl extends ServiceImpl<SysTaskDao, SysTask> implements SysTaskService {

    @Resource
    private JavaTestSchedulingConfigurer javaTestSchedulingConfigurer;

    @Resource
    private ThreadPoolTaskExecutor taskExecutor;

    @Override
    public void startTask(Integer id) {
        // 查询任务信息
        SysTask sysTask = getById(id);
        // 如果任务注册成功
        if (javaTestSchedulingConfigurer.regTask(sysTask)) {
            taskExecutor.execute(() -> {
                // 修改任务状态
                sysTask.setStatus(1);
                updateById(sysTask);
            });
        }
    }

    @Override
    public void stopTask(Integer id) {
        javaTestSchedulingConfigurer.stop(id);
        // 主要业务的代码,不能放到多线程中
        // 主要业务执行之后的次要业务,比如说修改状态,添加一些附表的值/修改缓存等,可以放入子线程,用来提高效率
        taskExecutor.execute(()->{
        SysTask sysTask = new SysTask();
        sysTask.setId(id);
        sysTask.setStatus(0);
        updateById(sysTask);
        });
    }
}

调用/task/start/{id}和/task/stop/{id}接口,就可以操作定时任务启停了。


http://www.kler.cn/a/508056.html

相关文章:

  • LARGE LANGUAGE MODELS ARE HUMAN-LEVEL PROMPT ENGINEERS
  • 2025.1.16——三、supersqli 绕过|堆叠注入|handler查询法|预编译绕过法|修改原查询法
  • 从AI生成内容到虚拟现实:娱乐体验的新边界
  • MarsCode青训营打卡Day1(2025年1月14日)|稀土掘金-16.最大矩形面积问题
  • Linux中安装mysql8,很详细
  • 51单片机——DS18B20温度传感器
  • 二百八十三、Java——IDEA中通过快捷键查看某一类的定义位置
  • Linux下的dev,sys和proc(TODO)
  • OpenCV阈值
  • 【C语言】_内存拷贝函数memcpy与memmove
  • Matplotlib基础
  • 【Elasticsearch】搜索类型介绍,以及使用SpringBoot实现,并展现给前端
  • 深度学习基础--GRU学习笔记(李沐《动手学习深度学习》)
  • 如何用ChatGPT玩转知识图谱?
  • NLP意图识别数据集处理流程
  • PyTorch 神经协同过滤 (NCF) 推荐系统教程
  • 【 MySQL 学习3】查询
  • 当当网书籍信息爬虫
  • 【教程】windows下使用docker部署hyperf框架
  • Java最常用的几种设计模式详解及适用业务场景
  • 论文阅读:Structure-Driven Representation Learning for Deep Clustering
  • Vue2+OpenLayers实现折线绘制功能(提供Gitee源码)
  • MySQL 的mysql_secure_installation安全脚本执行过程介绍
  • jenkins-Job构建
  • 55.【5】BUUCTF WEB NCTF2019 sqli
  • 前端框架: Vue3组件设计模式