Timed task implementation: Timer, Quartz

The use and principle of Timer in JDK

Timer use

Scheduling one-off tasks

Execute after a specified delay

@Test
public void givenUsingTimer_whenSchedulingTaskOnce_thenCorrect() {
    
    
    TimerTask task = new TimerTask() {
    
    
        public void run() {
    
    
            System.out.println("Task performed on: " + new Date() + "n" +
              "Thread's name: " + Thread.currentThread().getName());
        }
    };
    Timer timer = new Timer("Timer");
    
    long delay = 1000L;
    timer.schedule(task, delay);
}

Execute at the specified time
When the second parameter is Data, it means to execute on the specified date.
insert image description here
Let's say we have an old legacy database and we want to migrate its data to a new database with a better schema. We can create a DatabaseMigrationTask class to handle that migration:

public class DatabaseMigrationTask extends TimerTask {
    
    
    private List<String> oldDatabase;
    private List<String> newDatabase;

    public DatabaseMigrationTask(List<String> oldDatabase, List<String> newDatabase) {
    
    
        this.oldDatabase = oldDatabase;
        this.newDatabase = newDatabase;
    }

    @Override
    public void run() {
    
    
        newDatabase.addAll(oldDatabase);
    }
}

For simplicity, we represent both databases as lists of strings. Simply put, our migration is to put the data in the first list into the second list. To execute this migration at the desired moment, we have to use the overloaded version of the schedule() method:

List<String> oldDatabase = Arrays.asList("Harrison Ford", "Carrie Fisher", "Mark Hamill");
List<String> newDatabase = new ArrayList<>();

LocalDateTime twoSecondsLater = LocalDateTime.now().plusSeconds(2);
Date twoSecondsLaterAsDate = Date.from(twoSecondsLater.atZone(ZoneId.systemDefault()).toInstant());

new Timer().schedule(new DatabaseMigrationTask(oldDatabase, newDatabase), twoSecondsLaterAsDate);

We give the migration task and execution date to the schedule() method. Then, execute the migration at the time indicated by twoSecondsLater:

while (LocalDateTime.now().isBefore(twoSecondsLater)) {
    
    
    assertThat(newDatabase).isEmpty();
    Thread.sleep(500);
}
assertThat(newDatabase).containsExactlyElementsOf(oldDatabase);

Scheduling repeatable tasks

The Timer class offers several possibilities: we can set the repetition to observe a fixed delay or a fixed frequency.

  • Fixed Delay: Means that execution will start some time after the last execution started, even if it is delayed (and thus itself delayed). Let's say we want to schedule a task every two seconds, the first execution takes one second, and the second execution takes two seconds, but with a delay of one second. Then, the third execution will start from the fifth second.

Let's schedule a second-by-second newsletter:

public class NewsletterTask extends TimerTask {
    
    
    @Override
    public void run() {
    
    
        System.out.println("Email sent at: " 
          + LocalDateTime.ofInstant(Instant.ofEpochMilli(scheduledExecutionTime()), 
          ZoneId.systemDefault()));
    }
}

Each time it executes, the task prints its scheduled time, which we collect using the TimerTask#scheduledExecutionTime() method. So what if we want to schedule this task every second in fixed delay mode? We have to use the overloaded version of schedule() mentioned earlier:

new Timer().schedule(new NewsletterTask(), 0, 1000);

for (int i = 0; i < 3; i++) {
    
    
    Thread.sleep(1000);
}

Of course, we only test for a few cases:

Email sent at: 2020-01-01T10:50:30.860
Email sent at: 2020-01-01T10:50:31.860
Email sent at: 2020-01-01T10:50:32.861
Email sent at: 2020-01-01T10:50:33.861

There is at least a second between each execution, but sometimes there is a millisecond delay. This behavior is due to our decision to repeat with a fixed delay.

  • Fixed frequency: means that each execution will adhere to the initial schedule, regardless of whether previous executions were delayed. Let's reuse the previous example, with a fixed frequency, the second task will start after 3 seconds (because of the delay). However, the third execution after four seconds (regarding the initial schedule of executing every two seconds).

To schedule a daily task:

@Test
public void givenUsingTimer_whenSchedulingDailyTask_thenCorrect() {
    
    
    TimerTask repeatedTask = new TimerTask() {
    
    
        public void run() {
    
    
            System.out.println("Task performed on " + new Date());
        }
    };
    Timer timer = new Timer("Timer");
    
    long delay = 1000L;
    long period = 1000L * 60L * 60L * 24L;
    timer.scheduleAtFixedRate(repeatedTask, delay, period);
}

Cancel scheduler and tasks

Call the TimerTask.cancel() method in the run() method's implementation of the TimerTask itself

@Test
public void givenUsingTimer_whenCancelingTimerTask_thenCorrect()
  throws InterruptedException {
    
    
    TimerTask task = new TimerTask() {
    
    
        public void run() {
    
    
            System.out.println("Task performed on " + new Date());
            cancel();
        }
    };
    Timer timer = new Timer("Timer");
    
    timer.scheduleAtFixedRate(task, 1000L, 1000L);
    
    Thread.sleep(1000L * 2);
}

Cancel the timer:
call Timer.cancel()

@Test
public void givenUsingTimer_whenCancelingTimer_thenCorrect() 
  throws InterruptedException {
    
    
    TimerTask task = new TimerTask() {
    
    
        public void run() {
    
    
            System.out.println("Task performed on " + new Date());
        }
    };
    Timer timer = new Timer("Timer");
    
    timer.scheduleAtFixedRate(task, 1000L, 1000L);
    
    Thread.sleep(1000L * 2); 
    timer.cancel(); 
}

Timing task thread pool

Quartz framework

Quartz is a lightweight task scheduling framework that only needs to define Job (task), Trigger (trigger) and Scheduler (scheduler) to realize a scheduled scheduling capability. Support database-based cluster mode, which can achieve idempotent execution of tasks.

Core class description

insert image description here

  • Scheduler: scheduler. All scheduling is controlled by it, it is the brain of Quartz, and all tasks are managed by it
  • Job: tasks, things that you want to execute regularly (define business logic)
  • JobDetail: Based on the Job, further packaging, which associates a Job, and specifies more detailed attributes for the Job, such as identification, etc.
  • Trigger: Trigger. Can be assigned to a task, specify the trigger mechanism of the task

Trigger trigger

SimpleTrigger

Tasks executed at certain time intervals (in milliseconds)

  • Specify start and end times (time ends)
  • Specify time interval, number of executions

insert image description here
insert image description here

CronTrigger (emphasis)

Cron expressions are mainly used for timing job (timed task) system to define the expression of execution time or execution frequency. You can use Cron expression to set the timing task to execute every day or every month and so on.
cron expression format:

{seconds} {minutes} {hours} {date} {month} {week} {year (can be empty)}

The CronTrigger constructed based on the Cron expression no longer uses SimpleTrigger's startNow(), endAt(), etc., only withSchedule(CronScheduleBuilder.cronSchedule("30 01 17 11 5 ?"))

insert image description here
It means that every minute and every second of every hour on May 11 will be executed once

CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity("trigger1","group1")
               		 .withSchedule(CronScheduleBuilder.cronSchedule(  "* * * 11 5 ?"))
              		 .build();

Executed every minute and every 2 seconds of the hour on May 11

CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity("trigger1","group1")
               		 .withSchedule(CronScheduleBuilder.cronSchedule(  "*/2 * * 11 5 ?"))
               		 .build();

Spring integrates Quartz (emphasis)

Task information SQL storage

There are two ways to store task information in Quartz, using memory or using a database. Here we use the MySQL database storage method. First, we need to create a new Quartz related table. The sql script download address: http://www.quartz-scheduler.org /downloads/, the name is tables_mysql.sql, after the creation is successful, there are 11 more tables in the database

Maven mainly depends on

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<!-- 5.1.* 版本适用于MySQL Server的5.6.*、5.7.*和8.0.* -->
<dependency>
	<groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
	<version>5.1.38</version>
</dependency>
<dependency>
	<groupId>com.alibaba</groupId>
	<artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.10</version>
</dependency>
<!--mybatis-->
<dependency>
	<groupId>org.mybatis.spring.boot</groupId>
	<artifactId>mybatis-spring-boot-starter</artifactId>
	<version>1.3.2</version>
</dependency>
<!--pagehelper分页-->
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
	<version>1.3.0</version>
</dependency>

Here, druid is used as the database connection pool, and Quartz uses c3p0 by default

configuration file

1. quartz.properties
By default, Quartz will load quartz.properties under the classpath as a configuration file. If not found, the quartz.properties file under org/quartz under the jar package of the quartz framework will be used.

#主要分为scheduler、threadPool、jobStore、dataSource等部分


org.quartz.scheduler.instanceId=AUTO
org.quartz.scheduler.instanceName=DefaultQuartzScheduler
#如果您希望Quartz Scheduler通过RMI作为服务器导出本身,则将“rmi.export”标志设置为true
#在同一个配置文件中为'org.quartz.scheduler.rmi.export'和'org.quartz.scheduler.rmi.proxy'指定一个'true'值是没有意义的,如果你这样做'export'选项将被忽略
org.quartz.scheduler.rmi.export=false
#如果要连接(使用)远程服务的调度程序,则将“org.quartz.scheduler.rmi.proxy”标志设置为true。您还必须指定RMI注册表进程的主机和端口 - 通常是“localhost”端口1099
org.quartz.scheduler.rmi.proxy=false
org.quartz.scheduler.wrapJobExecutionInUserTransaction=false


#实例化ThreadPool时,使用的线程类为SimpleThreadPool
org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
#threadCount和threadPriority将以setter的形式注入ThreadPool实例
#并发个数  如果你只有几个工作每天触发几次 那么1个线程就可以,如果你有成千上万的工作,每分钟都有很多工作 那么久需要50-100之间.
#只有1到100之间的数字是非常实用的
org.quartz.threadPool.threadCount=5
#优先级 默认值为5
org.quartz.threadPool.threadPriority=5
#可以是“true”或“false”,默认为false
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread=true


#在被认为“misfired”(失火)之前,调度程序将“tolerate(容忍)”一个Triggers(触发器)将其下一个启动时间通过的毫秒数。默认值(如果您在配置中未输入此属性)为60000(60秒)
org.quartz.jobStore.misfireThreshold=5000
# 默认存储在内存中,RAMJobStore快速轻便,但是当进程终止时,所有调度信息都会丢失
#org.quartz.jobStore.class=org.quartz.simpl.RAMJobStore

#持久化方式,默认存储在内存中,此处使用数据库方式
org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
#您需要为JobStore选择一个DriverDelegate才能使用。DriverDelegate负责执行特定数据库可能需要的任何JDBC工作
# StdJDBCDelegate是一个使用“vanilla”JDBC代码(和SQL语句)来执行其工作的委托,用于完全符合JDBC的驱动程序
org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
#可以将“org.quartz.jobStore.useProperties”配置参数设置为“true”(默认为false),以指示JDBCJobStore将JobDataMaps中的所有值都作为字符串,
#因此可以作为名称 - 值对存储而不是在BLOB列中以其序列化形式存储更多复杂的对象。从长远来看,这是更安全的,因为您避免了将非String类序列化为BLOB的类版本问题
org.quartz.jobStore.useProperties=true
#表前缀
org.quartz.jobStore.tablePrefix=QRTZ_
#数据源别名,自定义
org.quartz.jobStore.dataSource=qzDS


#使用阿里的druid作为数据库连接池
org.quartz.dataSource.qzDS.connectionProvider.class=org.example.config.DruidPoolingconnectionProvider
org.quartz.dataSource.qzDS.URL=jdbc:mysql://127.0.0.1:3306/test_quartz?characterEncoding=utf8&useSSL=false&autoReconnect=true&serverTimezone=UTC
org.quartz.dataSource.qzDS.user=root
org.quartz.dataSource.qzDS.password=123456
org.quartz.dataSource.qzDS.driver=com.mysql.jdbc.Driver
org.quartz.dataSource.qzDS.maxConnections=10
#设置为“true”以打开群集功能。如果您有多个Quartz实例使用同一组数据库表,则此属性必须设置为“true”,否则您将遇到破坏
#org.quartz.jobStore.isClustered=false

Detailed explanation on configuration: https://blog.csdn.net/zixiao217/article/details/53091812

You can also check the official website: http://www.quartz-scheduler.org/documentation/2.3.1-SNAPSHOT/

2、application.properties

server.port=8080

#JDBC 配置:MySQL Server 版本为 5.7.35
spring.datasource.druid.url=jdbc:mysql://127.0.0.1:3306/test_quartz?characterEncoding=utf8&useSSL=false&autoReconnect=true&serverTimezone=UTC
spring.datasource.druid.username=root
spring.datasource.druid.password=123456
spring.datasource.druid.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource

#druid 连接池配置
spring.datasource.druid.initial-size=3
spring.datasource.druid.min-idle=3
spring.datasource.druid.max-active=10
spring.datasource.druid.max-wait=60000

#指定 mapper 文件路径
mybatis.mapper-locations=classpath:org/example/mapper/*.xml
mybatis.configuration.cache-enabled=true
#开启驼峰命名
mybatis.configuration.map-underscore-to-camel-case=true
#打印 SQL 语句
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

Quartz configuration class

@Configuration
public class QuartzConfig implements SchedulerFactoryBeanCustomizer {
    
    

    @Bean
    public Properties properties() throws IOException {
    
    
        PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean();
        // 对quartz.properties文件进行读取
        propertiesFactoryBean.setLocation(new ClassPathResource("/quartz.properties"));
        // 在quartz.properties中的属性被读取并注入后再初始化对象
        propertiesFactoryBean.afterPropertiesSet();
        return propertiesFactoryBean.getObject();
    }

    @Bean
    public SchedulerFactoryBean schedulerFactoryBean() throws IOException {
    
    
        SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
        schedulerFactoryBean.setQuartzProperties(properties());
        return schedulerFactoryBean;
    }

    /*
     * quartz初始化监听器
     */
    @Bean
    public QuartzInitializerListener executorListener() {
    
    
        return new QuartzInitializerListener();
    }

    /*
     * 通过SchedulerFactoryBean获取Scheduler的实例
     */
    @Bean
    public Scheduler scheduler() throws IOException {
    
    
        return schedulerFactoryBean().getScheduler();
    }

    /**
     * 使用阿里的druid作为数据库连接池
     */
    @Override
    public void customize(@NotNull SchedulerFactoryBean schedulerFactoryBean) {
    
    
        schedulerFactoryBean.setStartupDelay(2);
        schedulerFactoryBean.setAutoStartup(true);
        schedulerFactoryBean.setOverwriteExistingJobs(true);
    }
}

business use

Step 1: Create a task class:

@Slf4j
public class HelloJob implements Job {
    
    

    @Override
    public void execute(JobExecutionContext jobExecutionContext) {
    
    
        QuartzService quartzService = (QuartzService) SpringUtil.getBean("quartzServiceImpl");
        PageInfo<JobAndTriggerDto> jobAndTriggerDetails = quartzService.getJobAndTriggerDetails(1, 10);
        log.info("任务列表总数为:" + jobAndTriggerDetails.getTotal());
        log.info("Hello Job执行时间: " + DateUtil.now());
    }
}

Guess you like

Origin blog.csdn.net/baiduwaimai/article/details/132050546
Recommended