我正在参加「掘金·启航计划」
任务执行和调度
有些功能并不是通过浏览器主动的访问服务器的,有些功能是服务器定时启动定时运行的,比如:每隔一个小时算一下帖子的分数,每隔半个小时清理一下临时存的文件,这样的需求都需要任务调度的组件去解决。很显然这个任务调度的组件应该是基于多线程的,自动运行肯定是启动一个线程,那个线程独立的运行,我们在程序中但凡要用到多线程,一定是通过线程池使用的,因为我们创建一个线程是有开销的,并且开销比较大,使用线程池去管理这个线程能够让线程复用,提高处理能力,可以节约一些资源。
Spring中
- ThreadPoolTaskExecutor 普通的线程池
- ThreadPoolTaskScheduler 定时的线程池 (但是这个线程池分布式下可能存在问题)
所以分布式下使用 Spring Quartz 做定时任务更多一点
测试的时候记得启动kafka(虽然测试用不到,但是因为配置了kafka,不启动会导致测试失败)
JDK的线程池
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class ThreadPoolTests {
private static final Logger logger = LoggerFactory.getLogger(ThreadPoolTests.class);
// JDK普通线程池
private ExecutorService executorService = Executors.newFixedThreadPool(5); // 包含5个线程,反复复用这5个
// JDK可执行定时任务的线程池
private ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);//5个线程
private void sleep(long m) { // 让当前线程阻塞一会,因为test方法和main方法不一样,在main方法中一个执行完会等另一个
try { // 而在test中是并发的,一个执行完直接就结束了,我们让线程阻塞一下等其他线程执行完
Thread.sleep(m);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 1.JDK普通线程池
@Test
public void testExecutorService() {
Runnable task = new Runnable() {
@Override
public void run() {
logger.debug("Hello ExecutorService");
}
};
for (int i = 0; i < 10; i++) {
executorService.submit(task); // 调submit方法分配一个线程执行task
}
sleep(10000);
}
// 2.JDK定时任务线程池
@Test
public void testScheduledExecutorService() {
Runnable task = new Runnable() {
@Override
public void run() {
logger.debug("Hello ScheduledExecutorService");
}
};
// 延迟 10000 毫秒执行,反复执行时的间间隔1000毫秒 后面的 TimeUnit.MILLISECONDS 表示单位是毫秒
scheduledExecutorService.scheduleAtFixedRate(task, 10000, 1000, TimeUnit.MILLISECONDS);
sleep(30000);
}
}
Spring的线程池
使用Spring线程池时直接注入并且在application.properties做一些配置才可以(配一下启动时候带几个线程)
Spring 普通的线程池 ThreadPoolTaskExecutor 比 JDK 自带的线程池更灵活
Spring 可以定时的线程池 ThreadPoolTaskScheduler
# TaskExecutionProperties Spring的普通线程池配置
# 下面的配置是 核心线程数量是5,不够用时最多扩容到15,到了15还是不够用会把任务先放到队列里然后空闲时再取,缓冲作用,队列内最多缓冲100个
spring.task.execution.pool.core-size=5
spring.task.execution.pool.max-size=15
spring.task.execution.pool.queue-capacity=100
# TaskSchedulingProperties Spring的能启动定时任务的线程池,下面的配置意思是线程池里装的数量是5
spring.task.scheduling.pool.size=5
另外 ThreadPoolTaskScheduler 要生效还要写一个配置类 ThreadPoolConfig
@Configuration
@EnableScheduling // 启用定时任务
@EnableAsync // 加上这个注解可以使用简便方式调用
public class ThreadPoolConfig {
}
测试
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class ThreadPoolTests {
private static final Logger logger = LoggerFactory.getLogger(ThreadPoolTests.class);
// Spring普通线程池
@Autowired
private ThreadPoolTaskExecutor taskExecutor;
// Spring可执行定时任务的线程池
@Autowired
private ThreadPoolTaskScheduler taskScheduler;
private void sleep(long m) { // 让当前线程阻塞一会,因为test方法和main方法不一样,在main方法中一个执行完会等另一个
try { // 而在test中是并发的,一个执行完直接就结束了,我们让线程阻塞一下等其他线程执行完
Thread.sleep(m);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 3.Spring普通线程池
@Test
public void testThreadPoolTaskExecutor() {
Runnable task = new Runnable() {
@Override
public void run() {
logger.debug("Hello ThreadPoolTaskExecutor");
}
};
for (int i = 0; i < 10; i++) {
taskExecutor.submit(task);
}
sleep(10000);
}
// 4.Spring定时任务线程池
@Test
public void testThreadPoolTaskScheduler() {
Runnable task = new Runnable() {
@Override
public void run() {
logger.debug("Hello ThreadPoolTaskScheduler");
}
};
Date startTime = new Date(System.currentTimeMillis() + 10000);
taskScheduler.scheduleAtFixedRate(task, startTime, 1000);
// 开始的时间:当前+10000ms 1000表示时间间隔为1000ms
sleep(30000);
}
}
简化版调用方式(这种简化方式也要配置application.properties像上面那样)
意思是只要在任意一个类里写一个方法,在这个方法上加上一个注解,它就可以在Spring线程池环境下去运行,相当于把这个写的方法作为线程体,
然后是application.properties配置类就像上面那样(虽然是简化版,但是配置方式还是和Spring中配置是一样的),因为之前已经配过,所以这里不用再配了
AlphaService
@Service
public class AlphaService {
private static final Logger logger = LoggerFactory.getLogger(AlphaService.class);
// Spring普通线程池,让该方法在多线程环境下,被异步的调用,和主线程异步执行,并发执行
@Async
public void execute1() {
logger.debug("execute1");
}
// Spring定时线程池执行简化操作
@Scheduled(initialDelay = 10000, fixedRate = 1000) // 延迟10000ms执行,时间间隔1000ms
public void execute2() {
logger.debug("execute2");
}
}
测试
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class ThreadPoolTests {
private static final Logger logger = LoggerFactory.getLogger(ThreadPoolTests.class);
// JDK普通线程池
private ExecutorService executorService = Executors.newFixedThreadPool(5); // 包含5个线程,反复复用这5个
// JDK可执行定时任务的线程池
private ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);//5个线程
// Spring普通线程池
@Autowired
private ThreadPoolTaskExecutor taskExecutor;
// Spring可执行定时任务的线程池
@Autowired
private ThreadPoolTaskScheduler taskScheduler;
@Autowired
private AlphaService alphaService;
private void sleep(long m) { // 让当前线程阻塞一会,因为test方法和main方法不一样,在main方法中一个执行完会等另一个
try { // 而在test中是并发的,一个执行完直接就结束了,我们让线程阻塞一下等其他线程执行完
Thread.sleep(m);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 5.Spring普通线程池(简化)
@Test
public void testThreadPoolTaskExecutorSimple() {
for (int i = 0; i < 10; i++) {
alphaService.execute1();
}
sleep(10000);
}
// 6.Spring定时任务线程池(简化)
@Test
public void testThreadPoolTaskSchedulerSimple() {
sleep(30000);
}
}
Spring Quartz
因为 Spring的定时任务线程池ThreadPoolTaskScheduler在分布式环境下存在问题,我们使用 Spring Quartz 来做定时任务线程池。
# 我们使用Quartz主要是三个方面
对Job实现类进行配置,这样quartz才能够读取底层信息,生成表里数据,让任务运行起来
用JobDetail接口来配置Job,比如 名字是什么,是哪个组,描述,以及相关参数的配置,
Trigger是触发器的意思,用来配Job什么时候运行,以什么样的频率反复运行,
Spring Quartz 依赖于数据库,它有一套表在用Spring Quartz 时需要提前去创建,
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
首先我们要新建一个类实现Job接口用来定义任务
public class AlphaJob implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println(Thread.currentThread().getName() + ": execute a quartz job.");
}
}
对上面的Job实现类进行配置,这样quartz才能够读取底层信息,生成表里数据,让任务运行起来
用JobDetail接口来配置Job,比如 名字是什么,是哪个组,描述,以及相关参数的配置,
Trigger是触发器的意思,用来配Job什么时候运行,以什么样的频率反复运行,
然后写一个配置类配置JobDetail和Trigger
// 配置 -> 数据库 -> 调用
public class QuartzConfig {
// FactoryBean可简化Bean的实例化过程:
// 1.通过FactoryBean封装Bean的实例化过程.
// 2.将FactoryBean装配到Spring容器里.
// 3.将FactoryBean注入给其他的Bean.
// 4.该Bean得到的是FactoryBean所管理的对象实例.
// 配置JobDetail
@Bean
public JobDetailFactoryBean alphaJobDetail() {
JobDetailFactoryBean factoryBean = new JobDetailFactoryBean();
factoryBean.setJobClass(AlphaJob.class);
factoryBean.setName("alphaJob");
factoryBean.setGroup("alphaJobGroup");
factoryBean.setDurability(true); // 持久运行
factoryBean.setRequestsRecovery(true); // 任务可恢复
return factoryBean;
}
// 配置Trigger(SimpleTriggerFactoryBean, CronTriggerFactoryBean)
@Bean
public SimpleTriggerFactoryBean alphaTrigger(JobDetail alphaJobDetail) {
SimpleTriggerFactoryBean factoryBean = new SimpleTriggerFactoryBean();
factoryBean.setJobDetail(alphaJobDetail);
factoryBean.setName("alphaTrigger");
factoryBean.setGroup("alphaTriggerGroup");
factoryBean.setRepeatInterval(3000); // 多久执行一次
factoryBean.setJobDataMap(new JobDataMap());
return factoryBean;
}
}
然后Quartz还要做一个配置,在application.properties做配置
# QuartzProperties配置Quartz
# 下面配置的意思是
# 底层是jdbc
# communityScheduler是调度器名字
# 调度器id自动生成
# 用org.quartz.impl.jdbcjobstore.JobStoreTX将任务存到数据库
# 使用 org.quartz.impl.jdbcjobstore.StdJDBCDelegate 这个jdbc驱动,
# 采用集群方式
# 用org.quartz.simpl.SimpleThreadPool这个线程池
# 线程数量5
spring.quartz.job-store-type=jdbc
spring.quartz.scheduler-name=communityScheduler
spring.quartz.properties.org.quartz.scheduler.instanceId=AUTO
spring.quartz.properties.org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
spring.quartz.properties.org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
spring.quartz.properties.org.quartz.jobStore.isClustered=true
spring.quartz.properties.org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
spring.quartz.properties.org.quartz.threadPool.threadCount=5
然后只要一启动项目就会输出上面Job中定义的任务,会将数据存到数据库里,之后直接从数据库取,如果以后我们不想输出下面这句话,要么清空相应的表,要么执行程序清空
System.out.println(Thread.currentThread().getName() + ": execute a quartz job.");
下面我们演示一下删除任务的api是啥
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class QuartzTests {
@Autowired
private Scheduler scheduler; // 注入调度器
@Test
public void testDeleteJob() {
try {
// 删除哪个名字的Job和以及这个Job在哪个组名
boolean result = scheduler.deleteJob(new JobKey("alphaJob", "alphaJobGroup"));
System.out.println(result);
} catch (SchedulerException e) {
e.printStackTrace();
}
}
}
注:如果上面方法删除不了直接清空相应的数据库表,然后注释掉相关的方法