赞
踩
通常在生产环境中,不会存在单体的应用,如一个订单服务,可能同时部署多个相同的服务到不同的服务上,从而形成集群。此时定时任务就会面临重复执行的问题
;
业务系统是集群的 共用一个quartz 的数据库,存在的问题:
显然对于有状态的数据并不能重复执行,比如扣款,转账等等;对于无状态的数据重复执行也没有意义,只是增加了资源的开销而已;
只让集群中的一台服务器去跑定时任务(浪费其他机器的性能)
zookeeper 对集群的相同服务选举出来一个master 节点进行任务的执行:
集群中的每台服务器都会执行定时任务,但是一个任务只会分配到集群中的一台机器上:
quartz 支持;将定时任务放在jdbc 中进行存储,从而实现 故障转移,和负载均衡:
故障转移:
一个任务在一台机器上跑但是 这台机器下线;将任务分配到第二台机器执行(可以自行配置);
负载均衡:
将任务尽可能分配到各个服务中,但是每个任务只会被分配一次;
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-quartz</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.1.1</version> </dependency> <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.21</version> </dependency>
tip :本文springboot 版本如下:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.16</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
对应自动引入的quartz 版本为: org.quartz-scheduler:quartz:2.3.2
application.yml
server: port: 8080 spring: datasource: type: com.zaxxer.hikari.HikariDataSource #数据源类型 hikari: pool-name: KevinHikariPool #连接池名称,默认HikariPool-1 maximum-pool-size: 20 #最大连接数,小于等于0会被重置为默认值10;大于零小于1会被重置为minimum-idle的值 connection-timeout: 60000 #连接超时时间:毫秒,小于250毫秒,否则被重置为默认值30秒 minimum-idle: 10 #最小空闲连接,默认值10,小于0或大于maximum-pool-size,都会重置为maximum-pool-size idle-timeout: 500000 # 只有空闲连接数大于最大连接数且空闲时间超过该值,才会被释放 max-lifetime: 600000 #连接最大存活时间.不等于0且小于30秒,会被重置为默认值30分钟.设置应该比mysql设置的超时时间短 connection-test-query: SELECT 1 #连接测试查询 quartz: driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://localhost:3406/quartz-oneself?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&useAffectedRows=true&useSSL=false&zeroDateTimeBehavior=convertToNull&serverTimezone=GMT%2B8 username: root password: 123456 quartz: # 配置使用jdbc 存储job job-store-type: jdbc # 随着容器启动,启动定时任务(默认值ture) auto-startup: true # 是否可以覆盖定时任务,true 是 (默认值false) overwrite-existing-jobs: false # 在容器关闭时,任务执行后关闭容 (默认值false) wait-for-jobs-to-complete-on-shutdown: true # 定时任务延时启动的时间 (默认值0s) startup-delay: 10s properties: # 配置定时任务执行的线程池个数(默认10个) org.quartz.threadPool.threadCount: 10 # 配置集群的名称,同一个集群内的多个服务需要保证名称一致 org.quartz.scheduler.instanceName: OrderService # 集群中单个服务的实例id ,同一个集群中 实例id 需要不相同 org.quartz.scheduler.instanceId: Order_0 # 标识以集群的方式启动 org.quartz.jobStore.isClustered: true # 存储job 时使用的事务管理类,注意改参数 不同版本设置的值 有差异 org.quartz.jobStore.class: org.springframework.scheduling.quartz.LocalDataSourceJobStore # 数据库驱动,用来匹配不同数据的实现类 org.quartz.jobStore.driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate # quartz集群 定时任务集群表前缀 org.quartz.jobStore.tablePrefix: QRTZ_ # 容许的调度引擎设置触发器超时的"临界值"。任务的超时容忍度 默认为60秒(这里单位为毫秒) org.quartz.jobStore.misfireThreshold: 12000 jdbc: # initialize-schema: always initialize-schema: never
注意:
(1)org.quartz.jobStore.class 低版本、高版本取值不同:
低版本:2.2.6.Release
org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
高版本:2.5.x- 2.7.18
org.quartz.jobStore.class=org.springframework.scheduling.quartz.LocalDataSourceJobStore
避免错误信息:org.quartz.SchedulerConfigException: DataSource name not set.
(2) org.quartz.jobStore.tablePrefix 定时任务集群表前缀,需要遵从一定规范:
qrtz_ 后的内容不能进行修改
;
QuartzDataSourceConfig.java:
@Configuration public class QuartzDataSourceConfig { @Value("${spring.datasource.quartz.jdbc-url}") private String url; @Value("${spring.datasource.quartz.driver-class-name}") private String driverClassName; @Value("${spring.datasource.quartz.username}") private String username; @Value("${spring.datasource.quartz.password}") private String password; @Autowired private HikariBaseConfig hikariBaseConfig; @Bean // 标识quartz 数据源 @QuartzDataSource @Qualifier("quartzDataSource") public DataSource quartzDataSource() { return hikariBaseConfig.getDataSource(driverClassName, url, username, password); } }
业务类 HelloService:
@Service
public class HelloService {
public String hello(){
return "hello";
}
}
业务类QuartzTest:
import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.quartz.QuartzJobBean; import java.text.SimpleDateFormat; import java.util.Date; import java.util.StringJoiner; public class QuartzTest extends QuartzJobBean { @Autowired private HelloService helloService; @Override protected void executeInternal(JobExecutionContext context) throws JobExecutionException { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // System.out.println("\"job 执行\" = " + "job 执行" + sdf.format(new Date())); StringJoiner outStr = new StringJoiner("") .add("QuartzTest 执行") .add(sdf.format(new Date())) .add(Thread.currentThread().getName()) .add(context.getTrigger().getKey().getName()) .add(helloService.toString()) .add(helloService.hello()); System.out.println(outStr); } }
业务类 JobConfigure:
import org.quartz.*; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class JobConfigure { @Bean public JobDetail jobDetail1() { return JobBuilder.newJob(QuartzTest.class) .usingJobData("job", "jobDetail") .usingJobData("name", "jobDetail") .usingJobData("count", 0) .storeDurably() .withIdentity("jobConfigure", "group1").build(); } @Bean public Trigger trigger1() { return TriggerBuilder.newTrigger() .withIdentity("triggerConfigure", "trigger1") .forJob("jobConfigure","group1") .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(1) .repeatForever()) .usingJobData("trigger", "trigger") .usingJobData("name", "trigger") .build(); } @Bean public JobDetail jobDetail2() { return JobBuilder.newJob(QuartzTest.class) .usingJobData("job", "jobDetail2") .usingJobData("name", "jobDetail2") .usingJobData("count", 0) .storeDurably() .withIdentity("jobConfigure2", "group1").build(); } @Bean public Trigger trigger2() { return TriggerBuilder.newTrigger() .withIdentity("triggerConfigure2", "trigger2") .forJob("jobConfigure2","group1") .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(2) .repeatForever()) .usingJobData("trigger", "trigger2") .usingJobData("name", "trigger2") .build(); } }
修改 application.yml 中 initialize-schema 为always ,让其可以帮忙在数据库中创建对应的表:
initialize-schema: always
可以看到服务是以集群方式启动的,并且对应的表已经完成了创建:
定时任务已经开始执行:
修改 application.yml 中 initialize-schema 为never:
initialize-schema: never
JobConfigure 去除@Configuration 注解,后续让项目从数据库中加载任务;
application-1.yml:
spring:
quartz:
properties:
org.quartz.scheduler.instanceName: OrderService
org.quartz.scheduler.instanceId: Order_1
org.quartz.jobStore.isClustered: true
org.quartz.threadPool.threadCount: 3
org.quartz.jobStore.class: org.springframework.scheduling.quartz.LocalDataSourceJobStore
org.quartz.jobStore.driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.tablePrefix: QRTZ_
org.quartz.jobStore.misfireThreshold: 12000
server:
port: 8081
application-2.yml
spring:
quartz:
properties:
org.quartz.scheduler.instanceName: OrderService
org.quartz.scheduler.instanceId: Order_2
org.quartz.jobStore.isClustered: true
org.quartz.threadPool.threadCount: 3
org.quartz.jobStore.class: org.springframework.scheduling.quartz.LocalDataSourceJobStore
org.quartz.jobStore.driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.tablePrefix: QRTZ_
org.quartz.jobStore.misfireThreshold: 12000
server:
port: 8082
点击 Edit Contigurations 配置选项:
选中需要启动多个实例的项目然后,点击 进行复制:
修改配置名称,然后在 Active: profiles 配置要生效的 配置文件,此处和 application-1.yml ,“-” 后面的内容保持一致;如果是 application-dev.yml ,那么此处填写的值为"dev";
选中配置文件后,启动项目; 注意项目启动时 ,也会去加载 application.yml 配置,如果配置相同 application-1.yml 会进行覆盖;
当正在执行任务的服务停掉后,后将任务转移至另外一个正常服务中:
quartz 的集群需要同一个服务的不同实例,都要连接到同一个 定时任务的数据源,并且通过 org.quartz.jobStore.isClustered: true 开启 集群,以实现定时任务的负载均衡和故障转移。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。