7.6 任务执行和调度

403 阅读5分钟

我正在参加「掘金·启航计划」

任务执行和调度

image-20220730074407613

有些功能并不是通过浏览器主动的访问服务器的,有些功能是服务器定时启动定时运行的,比如:每隔一个小时算一下帖子的分数,每隔半个小时清理一下临时存的文件,这样的需求都需要任务调度的组件去解决。很显然这个任务调度的组件应该是基于多线程的,自动运行肯定是启动一个线程,那个线程独立的运行,我们在程序中但凡要用到多线程,一定是通过线程池使用的,因为我们创建一个线程是有开销的,并且开销比较大,使用线程池去管理这个线程能够让线程复用,提高处理能力,可以节约一些资源。

Spring中

  • ThreadPoolTaskExecutor 普通的线程池
  • ThreadPoolTaskScheduler 定时的线程池 (但是这个线程池分布式下可能存在问题)

​ 所以分布式下使用 Spring Quartz 做定时任务更多一点

image-20220730101507337

测试的时候记得启动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

image-20220730141229258

另外 ThreadPoolTaskScheduler 要生效还要写一个配置类 ThreadPoolConfig

@Configuration
@EnableScheduling                   // 启用定时任务
@EnableAsync                        // 加上这个注解可以使用简便方式调用
public class ThreadPoolConfig {
}

image-20220730141302542

测试

@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线程池环境下去运行,相当于把这个写的方法作为线程体,

image-20220730100005654

然后是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 时需要提前去创建,

image-20220730101803369

引入依赖

<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.");
    }
}

image-20220730141441772

对上面的Job实现类进行配置,这样quartz才能够读取底层信息,生成表里数据,让任务运行起来

JobDetail接口来配置Job,比如 名字是什么,是哪个组,描述,以及相关参数的配置,

Trigger是触发器的意思,用来配Job什么时候运行,以什么样的频率反复运行,

然后写一个配置类配置JobDetailTrigger

// 配置 -> 数据库 -> 调用
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;
    }
}

image-20220730141550487

然后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

image-20220730141629183

然后只要一启动项目就会输出上面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();
        }
    }
}

注:如果上面方法删除不了直接清空相应的数据库表,然后注释掉相关的方法