专栏名称: Java极客技术
Java 人的社区,专注 Java 一百年!
目录
相关文章推荐
51好读  ›  专栏  ›  Java极客技术

从双十一凌晨准时开启秒杀,看任务调度实践历程

Java极客技术  · 公众号  ·  · 2020-11-14 07:30

正文

每天早上 七点三十 ,准时推送干货



一、介绍

说到定时任务,相信大家都不陌生,在我们实际的工作中,用到定时任务的场景可以说非常的多,例如:

  • 双 11 的 0 点,定时开启秒杀
  • 每月1号,财务系统自动拉取每个人的绩效工资,用于薪资计算
  • 使用 TCP 长连接时,客户端按照固定频率定时向服务端发送心跳请求

等等,定时器像水和空气一般,普遍存在于各个场景中,在实际的业务开发中,基本上少不了定时任务的应用。

总结起来,一般定时任务的表现有以下几个特征:

  • 1. 在某个时刻触发 ,例如11.11号0点开启秒杀
  • 2. 按照固定频率周期性触发 ,例如每分钟发送心跳请求
  • 3. 预约指定时刻开始周期性触发 ,例如从12.1号开始每天7点闹钟响起

说了这么多,也不BB了,下面我们就来点干活,列举一些实际项目中使用的相关工具!

二、crontab 定时器

2.1、介绍

crontab 严格来说并不是属于 java 内的,它是 linux 自带的一个工具,可以周期性地执行某个 shell 脚本或命令。

由于 crontab 在实际开发中应用比较多, 特别是对于运维的人,crontab 命令是必须用到的命令,自动化运维中一定少不了它,而且 crontab 表达式跟我们后面要介绍的其他定时任务框架,例如 Quartz,Spring Schedule 的 cron 表达式类似,所以这里先介绍 crontab。

简而言之,crontab 就是一个自定义定时器。

命令格式如下:

.---------------- minute (0 - 59)
|  .------------- hour (0 - 23)
|  |  .---------- day of month (1 - 31)
|  |  |  .------- month (1 - 12) OR jan,feb,mar,apr ...
|  |  |  |  .---- day of week (0 - 6) (Sunday=0 or 7) ...
|  |  |  |  |
*  *  *  *  *  command
  • 第一个参数(minute):代表一小时内的第几分,范围 0-59。
  • 第二个参数(hour):代表一天中的第几小时,范围 0-23。
  • 第三个参数(day):代表一个月中的第几天,范围 1-31。
  • 第四个参数(month):代表一年中第几个月,范围 1-12。
  • 第五个参数(week):代表星期几,范围 0-7 (0及7都是星期天)。
  • 第六个参数(command):所要执行的指令。

具体应用的时候,时间定义大概是长下面这个样子!

# 每5分钟执行一次命令
*/5 * * * * Command
# 每小时的第5分钟执行一次命令
5 * * * * Command
# 指定每天下午的 6:30 执行一次命令
30 18 * * * Command
# 指定每月8号的7:30分执行一次命令
30 7 8 * * Command
# 指定每年的6月8日5:30执行一次命令
30 5 8 6 * Command
# 指定每星期日的6:30执行一次命令
30 6 * * 0 Command

2.2、具体示例

centOS 操作系统为例,创建一个定时任务,每分钟执行某个指定 shell 脚本,过程如下!

  • 首先安装 crond 相关服务
yum -y install cronie yum-cron
  • 编写一个输出当前时间到日志的 shell 脚本
#创建一个test.sh脚本
vim /root/shell/test.sh

#脚本内容如下,将内容输出到file.log文件
echo `date '+%Y-%m-%d %H:%M:%S'` >> /root/shell/file.log
  • 先执行一下脚本,观察内容是否输出到 file.log 文件
sh /root/shell/test.sh
  • 查看日志文件内容
cat /root/shell/file.log

如果出现以下内容,说明运行正常!

  • 接着再来创建一个定时任务,每分钟执行一次 test.sh





    
#编辑定时任务【删除-添加-修改】
crontab -e
  • 在文件末尾加入如下信息
#每分钟执行一次test.sh脚本
*/1 * * * * sh /root/shell/test.sh
  • 如果想查定时任务是否加入,可以通过如下命令
#查看crontab定时任务
crontab -l
  • 最后就是重启定时任务
##重新载入配置
systemctl reload crond
#重启服务
systemctl restart crond
  • 查看 file.log 文件实时输出内容,观察 test.sh 脚本是否被执行
tail -f /root/shell/file.log

从结果上看,运行正常!

  • 如果想查看定时任务日志,可通过如下命令进行查看
tail -f /var/log/cron

如果想移除某个定时任务,直接输入 crontab -e 命令,并移除对应的脚本,然后刷新配置、重启服务即可!

如果想深入的了解 crontab 使用,可以参考这篇文章,里面总结得比较好, 这里就不再多说了。

三、java 自带的定时器

3.1、Timer

Timer 定时器,由 jdk 提供的 java.util.Timer java.util.TimerTask 两个类组合实现。

其中 TimerTask 表示某个具体任务,而 Timer 则是进行调度任务处理。

实现过程也很简单,示例如下:

import java.util.Timer;
import java.util.TimerTask;

public class TimerTest extends TimerTask {

    private String jobName;

    public TimerTest(String jobName) {
        this.jobName = jobName;
    }

    @Override
    public void run() {
        System.out.println("execute " + jobName);
    }

    public static void main(String[] args) {
        Timer timer = new Timer();
        long delay1 = 1 * 1000;
        long period1 = 1 * 1000;
        // 从现在开始 1 秒钟之后,每隔 1 秒钟执行一次 job1
        timer.schedule(new TimerTest("job1"), delay1, period1);

        long delay2 = 2 * 1000;
        long period2 = 2 * 1000;
        // 从现在开始 2 秒钟之后,每隔 2 秒钟执行一次 job2
        timer.schedule(new TimerTest("job2"), delay2, period2);
    }
}

输出结果:

execute job1
execute job2
execute job1
execute job1
execute job2
execute job1
execute job1
execute job2
...

Timer 的优点在于简单易用,由于所有任务都是由同一个线程来调度, 因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务

具体原因如下:

  • 1.当一个线程抛出异常时,整个 Timer 都会停止运行。例如上面的 job1 抛出异常的话,job2 也不会再跑了
  • 当一个线程里面处理的时间非常长的时候,会影响其他 job 的调度。例如,如果 job1 处理的时间要 1 分钟, 那么 job2 至少要等 1 分钟之后才能跑。

基于上面的原因,Timer 现在生产环境中都不在使用!

3.2、ScheduledExecutor

鉴于 Timer 的上述缺陷,从 Java 5 开始,推出了基于线程池设计的 ScheduledExecutor。

其设计思想是,每一个被调度的任务都会由线程池中一个线程来管理执行,因此任务是并发执行的,相互之间不会受到干扰。需要注意的是,只有当任务的执行时间到来时,ScheduedExecutor 才会真正启动一个线程,其余时间 ScheduledExecutor 都是在轮询任务的状态。

实现过程,示例如下:

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ScheduledExecutorTest implements Runnable {

    private String jobName;

    public ScheduledExecutorTest(String jobName) {
        this .jobName = jobName;
    }

    @Override
    public void run() {
        System.out.println("execute " + jobName);
    }

    public static void main(String[] args) {
        //设置10个核心线程
        ScheduledExecutorService service = Executors.newScheduledThreadPool(10);

        long initialDelay1 = 1;
        long period1 = 1;

        // 从现在开始1秒钟之后,每隔1秒钟执行一次job1
        service.scheduleAtFixedRate(
                new ScheduledExecutorTest("job1"), initialDelay1,
                period1, TimeUnit.SECONDS);

        long initialDelay2 = 2;
        long delay2 = 2;
        // 从现在开始2秒钟之后,每隔2秒钟执行一次job2
        service.scheduleWithFixedDelay(
                new ScheduledExecutorTest("job2"), initialDelay2,
                delay2, TimeUnit.SECONDS);
    }
}

输出结果:

execute job1
execute job1
execute job2
execute job1
execute job1
execute job2
execute job1

在 ScheduledExecutorService 中,由 initialDelay delay TimeUnit 三个参数决定任务的执行频率。

其中:

  • TimeUnit:表示执行的单位,例如:毫秒、秒、分、小时、天...
  • initialDelay:表示从现在开始多少 TimeUnit 后执行任务
  • delay:表示任务执行周期,每隔多少 TimeUnit 执行一次任务

从 api 上可以看到,ScheduledExecutorService 的出现,完全可以替代 Timer  ,同时完美的解决上面所说的 Timer 存在的两个问题!

  • 当任务抛异常时,即使异常没有被捕获, 线程池也还会新建线程,所以定时任务不会停止
  • 由于 ScheduledExecutorService 是不同线程处理不同的任务,因此不管一个线程的运行时间有多长,都不会影响到另外一个线程的运行

但是 ScheduledExecutorService 也不是万能的,比如我想每月1号统计一次报表、每季度月末统计销售额等等这样的需求。

你会发现使用 ScheduledExecutorService 实现的时候,每次任务执行之后,你需要从当前时间开始出下一次执行时间的间隔,而且每次都要重算,非常麻烦!

遇到这样的需求,就需要一个更加完善的任务调度框架来解决这些复杂的调度问题。

而我们所熟悉的开源框架 Quartz 在这方面就提供了强大的支持。

四、第三方定时器

4.1、Quartz

quartz 在 java 项目中应用非常的广,市面上很多的开源调度框架也基本都是直接或间接基于这个框架来开发的。

下面我们就通过一个例子,来简单地认识一下它。

  • 引入 quartz
<dependency>
    <groupId>org.quartz-schedulergroupId>
    <artifactId>quartzartifactId>
    <version>2.3.2version>
dependency>
  • 编写示例代码
public class QuartzTest implements Job {

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
    }

    public static void main(String[] args) throws SchedulerException {
        // 创建一个Scheduler
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

        // 启动Scheduler
        scheduler.start();

        // 新建一个Job, 指定执行类是QuartzTest, 指定一个K/V类型的数据, 指定job的name和group
        JobDetail job = JobBuilder.newJob(QuartzTest.class)
                .usingJobData("jobData", "test")
                .withIdentity("myJob", "myJobGroup")
                .build()
;

        // 新建一个Trigger, 表示JobDetail的调度计划, 这里的cron表达式是 每1秒执行一次
        Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("myTrigger""myTriggerGroup")
                .startNow()
                .withSchedule(CronScheduleBuilder.cronSchedule("0/5 * * * * ?"))
                .build();

        // 让scheduler开始调度这个job, 按trigger指定的计划
        scheduler.scheduleJob(job, trigger);
    }
}

输出结果:

2020-11-09 21:38:40
2020-11-09 21:38:45
2020-11-09 21:38:50
2020-11-09 21:38:55
2020-11-09 21:39:00
2020-11-09 21:39:05
2020-11-09 21:39:10
...

当然,你还可以从 JobExecutionContext 对象中,获取上文 usingJobData() 方法中设置的值。

@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
    //从context中获取instName,groupName以及dataMap
    String jobName = context.getJobDetail().getKey().getName();
    String groupName = context.getJobDetail().getKey().getGroup();
    JobDataMap dataMap = context.getJobDetail().getJobDataMap();
    //从dataMap中获取myDescription,myValue以及myArray
    String value = dataMap.getString("jobData");
    System.out.println("jobName:" + jobName + ",groupName:" + groupName + ",jobData:" + value);
}

输出结果:

jobName:myJob,groupName:myJobGroup,jobData:test

在 Quartz 工具包中,设计的核心类主要包括 Scheduler, Job 以及 Trigger。

  • Scheduler :可以理解为是一个具体的调度实例,用来调度任务
  • JobDetail :定义具体作业的实例,进一步封装和拓展了 Job 功能,其中 Job 是一个接口,类似上面的 TimerTask
  • Trigger :设置任务调度策略。例如多久执行一次,什么时候执行,以什么频率执行等等

相比 JDK 提供的任务调度服务,Quartz 最明显的一个特点就是 将任务调度者、任务具体实例、任务调度策略进行三方解耦 ,这么做的优点在于同一个 Job 可以绑定多个不同的 Trigger,同一个 Trigger 也可以调度多个 Job,配置灵活性非常强。

Trigger 同时还支持 cron 表达式,在任务调度时间配置方面,更加灵活。

当然,Quartz 的用途不仅仅在单例服务上,在分布式调度方面也同样应用非常广,由于篇幅原因,关于 Quartz 的详细使用介绍,我们会在后期的文章中详细深入分析。

4.2、Spring Schedule

与 Quartz 齐名的还有我们所熟悉的 Spring Schedule,由 Spring 原生提供支持。

实现上,Spring 中使用定时任务也非常简单。

4.2.1、基于 XML 配置
  • springApplication.xml 配置文件中加入如下信息
<beans xmlns="http://www.springframework.org/schema/beans"  
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"   
    xmlns:p="http://www.springframework.org/schema/p"  
    xmlns:task="http://www.springframework.org/schema/task"  
    xmlns:context="http://www.springframework.org/schema/context"  
    xmlns:aop="http://www.springframework.org/schema/aop"   
    xsi:schemaLocation="http://www.springframework.org/schema/beans   
    http://www.springframework.org/schema/beans/spring-beans-4.0.xsd  
    http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd    
    http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-4.0.xsd    
    http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd    
    http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd    
    http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-4.0.xsd"
>
  

   
    <task:annotation-driven />

   
    <bean id="myTask" class="com.spring.task.TestTask">bean>

 
    <task:scheduled-tasks>
        
        <task:scheduled ref="myTask" method="show" cron="*/5 * * * * ?" />
  
        <task:scheduled ref="myTask" method="print" cron="*/10 * * * * ?"/>
    task:scheduled-tasks>

    
    <context:component-scan base-package="com.spring.task" />

beans>
  • 定义自己的任务执行逻辑
package com.spring.task;

/**
 * 定义任务
 */

public class TestTask {

    public void show() {
        System.out.println("show method 1");
    }

    public void print() {
        System.out.println("print method 1");
    }
}
4.2.2、基于注解配置

基于注解的配置,可以直接在方法上配置相应的调度策略,相比 xml 的方式更加简洁。

  • 实现过程如下
package com.spring.task;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

/**
 * 基于注解的定时器
 */

@Component
public class TestTask2 {

    /**
     * 定时计算。每隔5秒执行一次
     */

    @Scheduled(cron = "*/5 * * * * ?")
    public void show() {
        System.out.println("show method 2"






请到「今天看啥」查看全文