定时任务的应用场景
- 每天凌晨2点,执行数据备份
- 心跳检测,每隔10s检测一下接口是否正常
- 每隔5分钟从淘宝、京东等商城抓取订单到自己的系统
- 每分钟检测超时订单(30分钟未支付订单),自动取消
主流定时任务框架
- 企业级别 Quartz
- Elastic-Job
认识Zookeeper及其搭建
分布式定时任务–Elastic-Job
认识 Elastic-Job
Elastic-Job是一个分布式调度解决方案,由两个相互独立的子项目Elastic-Job-Lite和Elastic-Job-Cloud组成。在这里我们着重介绍Elastic-Job-Lite。Elastic-Job-Lite定位为轻量级无中心化解决方案,使用jar包的形式提供最轻量级的分布式任务的协调服务,外部依赖仅Zookeeper。
- 分片概念
任务的分布式执行,需要将一个任务拆分为多个独立的任务项,然后由分布式的服务器分别执行某一个或几个分片项。
例如:有一个遍历数据库某张表的作业,现有2台服务器。为了快速的执行作业,那么每台服务器应执行作业的50%。 为满足此需求,可将作业分成2片,每台服务器执行1片。作业遍历数据的逻辑应为:服务器A遍历ID以奇数结尾的数据;服务器B遍历ID以偶数结尾的数据。 如果分成10片,则作业遍历数据的逻辑应为:每个服务得到的分片项应为ID%10,而服务器A被分配到分片项0,1,2,3,4;服务器B被分配到分片项5,6,7,8,9,直接的结果就是服务器A遍历ID以0-4结尾的数据;服务器B遍历ID以5-9结尾的数据。
Elastic-Job并不直接提供数据处理的功能,框架只会将分片项分配至各个运行中的作业服务器,开发者需要自行处理分片项与真实数据的对应关系。
- 作业高可用
如果在上述的作业中,如果有一个应用挂掉(总共有2个应用),分片项将会重新分片,剩下的唯一应用将获得分片项0-9。
Simple作业的springboot整合方式
- 引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.dangdang</groupId>
<artifactId>elastic-job-lite-core</artifactId>
<version>2.1.5</version>
</dependency>
<dependency>
<groupId>com.dangdang</groupId>
<artifactId>elastic-job-lite-spring</artifactId>
<version>2.1.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.6</version>
<exclusions>
<exclusion>
<artifactId>slf4j-log4j12</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
<exclusion>
<artifactId>log4j</artifactId>
<groupId>log4j</groupId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
@Slf4j
public class MySimpleJob implements SimpleJob {
@Override
public void execute(ShardingContext shardingContext) {
log.info("我是分片项:"+shardingContext.getShardingItem());
log.info("分片总数:"+shardingContext.getShardingTotalCount());
}
}
- 注解ElasticSimpleJob
package com.example.autoconfig;
import com.dangdang.ddframe.job.lite.api.listener.ElasticJobListener;
import com.dangdang.ddframe.job.lite.api.strategy.JobShardingStrategy;
import com.dangdang.ddframe.job.lite.api.strategy.impl.AverageAllocationJobShardingStrategy;
import org.springframework.stereotype.Component;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface ElasticSimpleJob {
String jobName() default "";
String cron() default "";
int shardingTotalCount() default 1;
boolean overwrite() default false;
Class<? extends JobShardingStrategy> jobStrategy() default AverageAllocationJobShardingStrategy.class;
boolean jobEvent() default false;
Class<? extends ElasticJobListener>[] jobListner() default {};
}
ElasticSimpleJob 自定义配置
package com.example.autoconfig;
import com.dangdang.ddframe.job.api.ElasticJob;
import com.dangdang.ddframe.job.api.simple.SimpleJob;
import com.dangdang.ddframe.job.config.JobCoreConfiguration;
import com.dangdang.ddframe.job.config.simple.SimpleJobConfiguration;
import com.dangdang.ddframe.job.event.JobEventConfiguration;
import com.dangdang.ddframe.job.event.rdb.JobEventRdbConfiguration;
import com.dangdang.ddframe.job.lite.api.listener.ElasticJobListener;
import com.dangdang.ddframe.job.lite.config.LiteJobConfiguration;
import com.dangdang.ddframe.job.lite.spring.api.SpringJobScheduler;
import com.dangdang.ddframe.job.reg.base.CoordinatorRegistryCenter;
import lombok.var;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import javax.sql.DataSource;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;
@Configuration
@ConditionalOnBean(CoordinatorRegistryCenter.class)
@AutoConfigureAfter(ZookeeperAutoConfig.class)
public class SimpleJobAutoConfig {
@Autowired
private CoordinatorRegistryCenter zkCenter;
@Autowired
private ApplicationContext applicationContext;
@Autowired
private DataSource dataSource;
@PostConstruct
public void initSimpleJob() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Map<String, Object> beans = applicationContext.getBeansWithAnnotation(ElasticSimpleJob.class);
for (Map.Entry<String, Object> entry : beans.entrySet()){
Object instance = entry.getValue();
Class<?>[] interfaces = instance.getClass().getInterfaces();
for (Class<?> superInterface : interfaces){
if (superInterface == SimpleJob.class){
ElasticSimpleJob annotation = instance.getClass().getAnnotation(ElasticSimpleJob.class);
String jobName = annotation.jobName();
String cron = annotation.cron();
int shardingTotalCount = annotation.shardingTotalCount();
boolean overwrite = annotation.overwrite();
Class<?> jobStrategy = annotation.jobStrategy();
boolean isJobEvent = annotation.jobEvent();
Class<? extends ElasticJobListener>[] listeners = annotation.jobListner();
ElasticJobListener[] listenerInstances = null;
if (listeners!=null && listeners.length>0){
listenerInstances = new ElasticJobListener[listeners.length];
int i = 0;
for (Class<? extends ElasticJobListener> listener : listeners){
ElasticJobListener listenerInstance = listener.getDeclaredConstructor().newInstance();
listenerInstances[i] = listenerInstance;
i++;
}
}else {
listenerInstances = new ElasticJobListener[0];
}
//job核心配置
var jcc = JobCoreConfiguration
.newBuilder(jobName,cron,shardingTotalCount)
.build();
//job类型配置
var jtc = new SimpleJobConfiguration(jcc,
instance.getClass().getCanonicalName());
//job根的配置(LiteJobConfiguration)
var ljc = LiteJobConfiguration
.newBuilder(jtc)
.jobShardingStrategyClass(jobStrategy.getCanonicalName())
.overwrite(overwrite)
.build();
// new JobScheduler(zkCenter,ljc).init();
if (isJobEvent){
JobEventConfiguration jec = new JobEventRdbConfiguration(dataSource);
new SpringJobScheduler((ElasticJob) instance,zkCenter,ljc,jec,listenerInstances).init();
}else {
new SpringJobScheduler((ElasticJob) instance,zkCenter,ljc,listenerInstances).init();
}
}
}
}
}
}
创建 src/main/resources/META-INF/spring.factories,自定义启动配置
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.autoconfig.ZookeeperAutoConfig,\
com.example.autoconfig.SimpleJobAutoConfig
调用 ElasticSimpleJob
package com.example.springbootelasticjob.job;
import com.dangdang.ddframe.job.api.ShardingContext;
import com.dangdang.ddframe.job.api.simple.SimpleJob;
import com.example.autoconfig.ElasticSimpleJob;
import com.example.springbootelasticjob.service.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Slf4j
@ElasticSimpleJob(
jobName = "mySimpleJob" ,
cron = "0/5 * * * * ?",
shardingTotalCount = 1,
overwrite = true
)
public class MySimpleJob implements SimpleJob {
@Autowired
private OrderService orderService;
@Override
public void execute(ShardingContext shardingContext) {
// log.info("我是分片项:"+shardingContext.getShardingItem());
for (int i=0;i<10;i++){
orderService.insertOrder();
}
}
}
- Simple作业的实战(30分钟未支付订单,自动取消)
1) springboot与mybatis整合
引入依赖
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.15</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
</dependencies>
MyBatis Generator插件自动生成数据库相关的文件
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.7</version>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.15</version>
</dependency>
</dependencies>
</plugin>
创建generatorConfig.xml文件,具体配置参考官网:http://mybatis.org/generator/index.html
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<context id="MysqlTables" targetRuntime="MyBatis3">
<jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
connectionURL="jdbc:mysql://127.0.0.1:3306/test?serverTimezone=Asia/Shanghai&useSSL=false"
userId="root"
password="xxxx">
<property name="nullCatalogMeansCurrent" value="true" />
</jdbcConnection>
<javaTypeResolver >
<property name="forceBigDecimals" value="false" />
</javaTypeResolver>
<javaModelGenerator targetPackage="com.example.springbootelasticjob.model" targetProject="src\main\java">
<property name="enableSubPackages" value="true" />
<property name="trimStrings" value="true" />
</javaModelGenerator>
<sqlMapGenerator targetPackage="mybatis" targetProject="src\main\resources">
<property name="enableSubPackages" value="true" />
</sqlMapGenerator>
<javaClientGenerator type="XMLMAPPER" targetPackage="com.example.springbootelasticjob.dao" targetProject="src\main\java">
<property name="enableSubPackages" value="true" />
</javaClientGenerator>
<table schema="dataflow" tableName="t_order" domainObjectName="Order" ></table>
<!-- <table schema="dataflow" tableName="all_order" domainObjectName="AllOrder" ></table>-->
<!-- <table schema="dataflow" tableName="jd_order" domainObjectName="JdOrder" ></table>-->
<!-- <table schema="dataflow" tableName="tmall_order" domainObjectName="TmallOrder" ></table>-->
</context>
</generatorConfiguration>
最后一步