快速上手SpringBoot定时任务

目录

一、创建SpringBoot定时任务

1、@SpringBootApplication启动

2、@EnableScheduling + @Scheduled 开启定时任务

3、cron表达式

二、常见问题

1、单线程任务丢失,转为异步线程池

2、关于分布式情况下,重复执行的问题(两种方案)

3、服务器宕机之后,丢失的任务如何补偿? 


一、创建SpringBoot定时任务

此文章在SpringBoot框架的基础上,创建SpringBoot定时任务,对于搭建SpringBoot框架可以阅读我的相关文章,也可以直接到Spring官网下载相关代码,在此不做赘述,直接上定时任务相关代码。

定时任务类:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.text.SimpleDateFormat;
import java.util.Date;

@Component
public class ScheduledTasks {

    private static final Logger log = LoggerFactory.getLogger(ScheduledTasks.class);

    private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");

    // 每隔5s执行一次
    @Scheduled(fixedRate = 5000)
    public void reportCurrentTime() {
        log.info("The time is now {}", dateFormat.format(new Date()));
    }
}

SpringBoot启动类

@SpringBootApplication
@EnableScheduling
public class MyApplication {
    public static void main(String[] args) {
        Logger logger = LoggerFactory.getLogger(MyApplication.class);
        try {
            SpringApplication.run(MyApplication.class);
            logger.info("springBoot启动成功...");
        } catch (Exception e) {
            logger.info("SpringBoot启动失败...");
        }
    }
}

以上代码是从官网Copy来的,是不是发现创建一个定时任务简直不要太简单?其实总的来说就两个注解而已:@EnableScheduling@Scheduled

下边来剖析下SpringBoot定时任务的相关的一些注解

1、@SpringBootApplication启动

之前用户使用的是3个注解注解他们的main类。分别是@Configuration,@EnableAutoConfiguration,@ComponentScan。由于这些注解一般都是一起使用,springboot提供了一个统一的注解@SpringBootApplication。

@SpringBootApplication = (默认属性)@Configuration + @EnableAutoConfiguration + @ComponentScan。

(1)@Configuration

提到@Configuration就要提到他的搭档@Bean。使用这两个注解就可以创建一个简单的spring配置类,可以用来替代相应的xml配置文件。

<beans> 
    <bean id = "car" class="com.test.Car"> 
        <property name="wheel" ref = "wheel"></property> 
    </bean> 
    <bean id = "wheel" class="com.test.Wheel"></bean> 
</beans> 

相当于

@Configuration 
public class Conf { 
    @Bean 
    public Car car() { 
        Car car = new Car(); 
        car.setWheel(wheel()); 
        return car; 
    } 
    @Bean  
    public Wheel wheel() { 
        return new Wheel(); 
    } 
}

@Configuration的注解类标识这个类可以使用Spring IOC容器作为bean定义的来源。@Bean注解告诉Spring,一个带有@Bean的注解方法将返回一个对象,该对象应该被注册为在Spring应用程序上下文中的bean

(2)@EnableAutoConfiguration

能够自动配置spring的上下文,试图猜测和配置你想要的bean类,通常会自动根据你的类路径和你的bean定义自动配置

(3)@ComponentScan

自动扫描指定包下的全部标有@Component的类并注册成bean,当然包括@Component下的子注解@Service@Repository@Controller

2、@EnableScheduling + @Scheduled 开启定时任务

(1)@EnableScheduling

@EnableScheduling 在配置类上使用,开启计划任务的支持,没有它,什么都无法安排

注:这个注解无论放在哪个类上,只要能检索到@Scheduled,就能开启定时任务

注解源码:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import({SchedulingConfiguration.class})
@Documented
public @interface EnableScheduling {
}

SchedulingConfiguration.class 类实现了Spring 的任务调度框架级功能。该配置类仅仅是定义了ScheduledAnnotationBeanPostProcessor 的实例。

其中SchedulingConfiguration.class 源码:

@Configuration
@Role(2)
public class SchedulingConfiguration {
    public SchedulingConfiguration() {
    }

    @Bean(
        name = {"org.springframework.context.annotation.internalScheduledAnnotationProcessor"}
    )
    @Role(2)
    public ScheduledAnnotationBeanPostProcessor scheduledAnnotationProcessor() {
        return new ScheduledAnnotationBeanPostProcessor();
    }
}

(2)@Scheduled

@Scheduled 用来在方法上申明这是一个计划任务,包括cronfixDelayfixRate等类型(需先开启计划任务的支持)。

使用fixedRate属性每隔固定时间执行,使用cron属性可按照指定时间执行(cron是UNIX和类UNIX(Linux)系统下的定时任务)。

源码如下:

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(Schedules.class)
public @interface Scheduled {
    String CRON_DISABLED = "-";

    String cron() default "";

    String zone() default "";

    long fixedDelay() default -1L;

    String fixedDelayString() default "";

    long fixedRate() default -1L;

    String fixedRateString() default "";

    long initialDelay() default -1L;

    String initialDelayString() default "";
}
  1. cron 是设置定时执行的表达式,如 0 0/5 * * * ?每隔五分钟执行一次
  2. zone 表示执行时间的时区,如"GMT+8",GMT是世界标准时间(格林威治时间),"GMT+8"表示东八区,即北京时间。
  3. fixedDelay 和 fixedDelayString 一个固定延迟时间执行,上个任务完成后,延迟多久执行
  4. fixedRate 和 fixedRateString 一个固定频率执行,上个任务开始后多长时间后开始执行
  5. initialDelay 和 initialDelayString 表示一个初始延迟时间,第一次被调用前延迟的时间

3、cron表达式

秒、分、时、日、月、周、年

cron表达式,有专门的语法,而且感觉有点绕人,不过简单来说,大家记住一些常用的用法即可,特殊的语法可以单独去查。
cron一共有7位,但是最后一位是年,可以留空,所以我们可以写6位。

* 第一位,表示秒,取值0-59
* 第二位,表示分,取值0-59
* 第三位,表示小时,取值0-23
* 第四位,日期天/日,取值1-31
* 第五位,日期月份,取值1-12
* 第六位,星期,取值1-7,星期一,星期二...,注:不是第1周,第二周的意思
          另外:1表示星期天,2表示星期一。
* 第7为,年份,可以留空,取值1970-2099

cron中,还有一些特殊的符号,含义如下:

(*)星号:可以理解为每的意思,每秒,每分,每天,每月,每年...
(?)问号:问号只能出现在日期和星期这两个位置。
(-)减号:表达一个范围,如在小时字段中使用“10-12”,则表示从10到12点,即10,11,12
(,)逗号:表达一个列表值,如在星期字段中使用“1,2,4”,则表示星期一,星期二,星期四
(/)斜杠:如:x/y,x是开始值,y是步长,比如在第一位(秒) 0/15就是,从0秒开始,每15秒,最后就是0,15,30,45,60    另:*/y,等同于0/y

corn表达式示例:

0 0 3 * * ?     每天3点执行
0 5 3 * * ?     每天3点5分执行
0 5 3 ? * *     每天3点5分执行,与上面作用相同
0 5/10 3 * * ?  每天3点的 5分,15分,25分,35分,45分,55分这几个时间点执行
0 10 3 ? * 1    每周星期天,3点10分 执行,注:1表示星期天    
0 10 3 ? * 1#3  每个月的第三个星期,星期天 执行,#号只能出现在星期的位置
             
每隔5秒执行一次:*/5 * * * * ?          
每隔1分钟执行一次:0 */1 * * * ?          
每天23点执行一次:0 0 23 * * ?         
每天凌晨1点执行一次:0 0 1 * * ?          
每月1号凌晨1点执行一次:0 0 1 1 * ?         
每月最后一天23点执行一次:0 0 23 L * ?          
每周星期天凌晨1点实行一次:0 0 1 ? * L           
在26分、29分、33分执行一次:0 26,29,33 * * * ?           
每天的0点、13点、18点、21点都执行一次:0 0 0,13,18,21 * * ?

二、常见问题

1、单线程任务丢失,转为异步线程池

默认的 ConcurrentTaskScheduler 计划执行器采用Executors.newSingleThreadScheduledExecutor() 实现单线程的执行器。因此,对同一个调度任务的执行总是同一个线程。如果任务的执行时间超过该任务的下一次执行时间,则会出现任务丢失,跳过该段时间的任务。上述问题有以下解决办法:

采用异步的方式执行调度任务,配置 Spring 的 @EnableAsync,在执行定时任务的方法上标注 @Async 配置任务执行池,线程池大小 n 的数量为:单个任务执行所需时间 / 任务执行的间隔时间。如下:

//每30秒执行一次
   @Async("taskExecutor")
   @Scheduled(fixedRate = 1000 * 3)
   public void reportCurrentTime(){
       System.out.println ("线程" + Thread.currentThread().getName() + "开始执行定时任务===&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&7&&&====》"
               + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
       long start = System.currentTimeMillis();
       Future<Boolean> isOk1;
       Future<Boolean> isOk2;
       ...省略...

2、关于分布式情况下,重复执行的问题(两种方案)

(1)使用redis分布式锁

可以使用redis的分布式锁保证spring schedule集群只执行一次。 redis分布式锁是通过setnx命令实现的。该命令的作用是,当往redis中存入一个值时,会先判断该值对应的key是否存在,如果存在则返回0,如果不存在,则将该值存入redis并返回1。(但是在分布式跨时区部署的时候,依然无法避免重复执行

@Component
@Configuration
@EnableScheduling
public class AutoConvertTask {
    private static final Logger logger = LoggerFactory.getLogger(AutoConvertTask.class);
 
    @Autowired
    private RedisTemplate redisTemplate;
 
    private static final String LOCK = "task-job-lock";
 
    private static final String KEY = "tasklock";
 
    @Scheduled(cron = "0 0 0 * * ? ")
    public void autoConvertJob() {
        boolean lock = false;
        try {
            lock = redisTemplate.opsForValue().setIfAbsent(KEY, LOCK);
            logger.info("是否获取到锁:" + lock);
            if (lock) {
                List<GameHistory> historyList = historyService.findTenDaysAgoUntreated();
                for (GameHistory history : historyList) {
                    update(history);
                }
            } else {
                logger.info("没有获取到锁,不执行任务!");
                return;
            }
        } finally {
            if (lock) {
                redisTemplate.delete(KEY);
                logger.info("任务结束,释放锁!");
            } else {
                logger.info("没有获取到锁,无需释放锁!");
            }
        }
 
    }
}

(2)使用shedlock将spring schedule上锁

可以通过使用shedlock将spring schedule上锁。详细见:https://segmentfault.com/a/1190000011975027

3、服务器宕机之后,丢失的任务如何补偿? 

可以将每次的任务执行时间缓在redis里,下次执行任务的时候都取出该时间,判断是否为上一个周期,如果不是,可以计算出中间丢失的周期数,然后做响应的补偿操作。如果怕redis宕机,可以将“执行时间”持久化到表中。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

swadian2008

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值