专注于Java领域优质,技术欢迎关注

作者: 鸭血粉丝 来自:Java极客技术

Hello~各位读者新年好,我是鸭血粉丝(大家可以称呼我为「阿粉」)。这里阿粉给大家拜个年,祝大家蒸蒸日上烫烫烫,年年有余屯屯屯。

那年那 Bug

春节放假,阿粉坐上高铁回家,路上阿粉突然想到一次生产问题。那是阿粉参加工作第一年,那一年国庆假期,阿粉提前一天请假回家办个护照。那时候阿粉刚开始负责的系统,所以工作日请假,还是有点担心,就怕问题看阿粉不在,悄然上门。

哎,真实越怕什么,就来什么。

高铁开到一半的时候,同事反馈系统不能获取最新的流水信息(流水信息通过 Spring 定时任务定时拉取)。阿粉心里一惊,立刻拔出电脑,连上 VPN,准备登上生产机器,查看系统情况。可是,高铁上网络大家也懂,很不稳定,连了好久连不上 VPN,只好远程指挥同事看一下系统日志。通过同事反馈的日志,发现拉取流水定时任务没有执行,进一步查看,阿粉发现整个系统其他的定时任务也都停止了。。。

这真是一个奇怪的的问题,这好端端的定时任务怎么会突然停止?

暂时想不到解决办法,只好指挥同事先重启应用。重启之后,暂时解决问题,定时任务重新开始执行,也获取到最新的付款流水信息。

问题排查

到家之后,阿粉立刻登上生产机器,查看系统日志。这里阿粉发现重启之前某一定时任务运行到一半,并且在这之后其他定时任务就没有再被执行。

通过系统日志,定位到了有问题的代码。

这里采用重试补偿策略,防止查询流水信息因为网络等问题发生偶发的失败。这个策略面对偶发的失败没什么问题,但是如果查询银行流水服务一直失败,这段代码就会陷入死循环。恰巧那段时间网络出现一些问题,导致这里查询一直处于失败。

增加最大重试次数,修复该 Bug。

修复之后,立刻将最新版本代码部署到生产系统,暂时解决了这个问题。

知识点:面对一些失败,可以采用重试补偿策略,重新执行,最大可能保证执行成功,但是这里切记设置合适的的重大的次数。

深入排查

虽然问题解决了,但是阿粉心里还是存在一个疑惑,为何一个定时任务发生了阻塞,就会影响执行其他定时任务。阿粉最初的理解是不同的定时任务应该互相隔离,互不影响才对,真难到是 Spring 定时任务的 Bug 吗?

想到这里,阿粉决定写一个 Demo,复现问题,然后深入源码排查。

启动程序,日志输出如下:

从日志可以看到,fixDelayMethod 方法执行之后进入休眠,直到休眠结束,cronMethod 定时任务才有机会被执行。另外从上面可以看到,上述两个定时任务都由 pool-1-thread-1线程执行。从这点可以看出 Spring 定时任务将会交给线程池执行。

知识点: 线程池中线程默认命名策略为 pool-%poolNumber-thread-%num。

如果线程池只有一个工作线程,该线程一旦被长时间阻塞,堆积的其他任务就没有机会被执行。

那么是不是这个问题导致的 Sping 定时任务停止执行?我们继续往下排查。

图上日志绿色部分, ScheduledAnnotationBeanPostProcessor 输出一个重要信息:

No TaskScheduler/ScheduledExecutorService bean found for scheduled processing

查看 Spring 文档,Spring 内部将会通过调用 TaskScheduler 执行定时任务,而另一个 ScheduledExecutorService 为 JDK 提供执行定时任务的执行器。记住这两者

通过这段日志,使用 IDEA 的强大的关键字搜索功能,定位到 ScheduledAnnotationBeanPostProcessor#finishRegistration 方法。

这个方法比较长,大家重点关注图中标示的几处。

Spring 启动之后将会扫描所有 Bean 中带有 @Scheduled 注解的方法,然后封装成 Task子类放置到 ScheduledTaskRegistrar。

这段代码位于 ScheduledAnnotationBeanPostProcessor#processScheduled,感兴趣的可以翻阅查看

如果此时 ScheduledTaskRegistrar 不存在定时任务或者 ScheduledTaskRegistrar 中的 TaskScheduler不存在,finishRegistration将会多次调用 ScheduledAnnotationBeanPostProcessor#resolveSchedulerBean 方法用以查找 TaskScheduler/ScheduledExecutorService。

接下去将会把获取到 Bean 通过 setScheduler 注入到 ScheduledTaskRegistrar 中。

如果获取的为 ScheduledExecutorService 类型,将会将其封装到 taskScheduler中。

最后还没找到,将会输出最刚开始见到的日志。然后 Spirng 将会在 ScheduledTaskRegistrar#afterPropertiesSet 创建一个单线程的定时任务执行器 ScheduledExecutorService,注入到 ConcurrentTaskScheduler中,然后通过 taskScheduler 执行定时任务。

image-20200125144040781

交给TaskScheduler 的定时任务最后实际上还是通过 ScheduledExecutorService执行。

这里可以得出一个结论:

Spring 定时任务实际上通过 JDK 提供的 ScheduledExecutorService执行。默认情况下,Spring 将会生成一个单线程ScheduledExecutorService执行定时任务。所以一旦某一个定时任务长时间阻塞这个执行线程,其他定时任务都将被影响,没有机会被执行线程执行。

Spring 这种默认配置,在需要执行多个定时任务的情况,可能会是一个坑。我们可以通过改变配置,使 Spring 采用多线程执行定时任务。

自定义配置

Spring 可以通过多种方式改变默认配置。

xml 配置

通过 xml 配置 TaskScheduler 线程数。

<task:annotation-driven scheduler="myScheduler"/>

<task:scheduler id="myScheduler" pool-size="10"/>

通过上面的配置,Spring 将会使用 TaskScheduler 子类 ThreadPoolTaskScheduler,内部线程数为 pool-size 数量,这个线程数将会直接设置 ScheduledExecutorService 线程数量。

注解配置

在上面问题排查中,我们知道 Spring 将会查找 TaskScheduler/ScheduledExecutorService,若存在将会使用。所以这里我们可以生成这些类的 Bean。

以上方式二选一即可

SpringBoot 配置

上面两种配置适用于普通 Spring,比较繁琐。相比而言 SpringBoot 配置将会非常简单,只需要在启动配置文件加入如下配置即可。

spring.task.scheduling.pool.size=10

spring.task.scheduling.thread-name-prefix=task-test

技术总结

下面开始技术总结:

  1. Spring 定时任务执行原理实际使用的是 JDK 自带的 ScheduledExecutorService
  2. Spring 默认配置下,将会使用具有单线程的 ScheduledExecutorService
  3. 单线程执行定时任务,如果某一个定时任务执行时间较长,将会影响其他定时任务执行
  4. 如果存在多个定时任务,为了保证定时任务执行时间的准确性,可以修改默认配置,使其使用多线程执行定时任务
  5. 面对偶发的失败,我们可以采用重试补偿策略,不过这里切记设置合适的最大重试次数

随便聊聊

对于常用的开源框架,我们不仅要掌握怎么用,还要熟悉相关的配置,最后还应该去了解其内部的使用的原理。这样出了问题,我们也能很快定位问题,找到问题的实际原因。

推荐:盐亭县属于哪个市

安义县属于哪个市?

定时任务莫名停止,Spring 定时任务存在 Bug?的更多相关文章

  1. 定时任务莫名停止,Spring 定时任务存在 Bug??

    Hello~各位读者新年好!这里楼下小黑哥给大家拜个年,祝大家蒸蒸日上烫烫烫,年年有余屯屯屯. 那年那 Bug 春节放假,小黑哥坐上高铁回家,突然想到一次生产问题.那是小黑哥参加工作第一年,那一年国庆 ...

  2. Spring定时任务有时候会莫名奇妙的终止?

    最近在是使用Spring配置定时定时任务(基于xml配置使用spring自带的定时任务),一开始使用没什么问题当使用久了就会出现有些定时任务自动停止了.(关于如何使用以及如何它的原理是啥,这里不进行阐 ...

  3. spring定时任务详解(@Scheduled注解)( 转 李秀才的博客 )

    在springMVC里使用spring的定时任务非常的简单,如下: (一)在xml里加入task的命名空间 xmlns:task="http://www.springframework.or ...

  4. 摆脱Spring 定时任务的@Scheduled cron表达式的困扰

    一.背景 最近因为需要,需要适用Spring的task定时任务进行跑定时任务,以前也接触过,但是因为懒没有好好地理解@Scheduled的cron表达式,这次便对它做了一个全方位的了解和任务,记录下来 ...

  5. spring 定时任务配置

    1.(易)如何在spring中配置定时任务? spring的定时任务配置分为三个步骤: 1.定义任务 2.任务执行策略配置 3.启动任务 (程序中一般我们都是到过写的,直观些) 1.定义任务 < ...

  6. spring定时任务

    需求:在tomcat启动时开启一个定时任务,即项目启动完成后,自动执行某一特定任务. 想法:容器启动时执行方法,最容易想到的就是servlet中可以配置load-on-startup,设置一个正整数也 ...

  7. spring定时任务(转载)

    在springMVC里使用spring的定时任务非常的简单,如下: (一)在xml里加入task的命名空间 xmlns:task="http://www.springframework.or ...

  8. Spring定时任务的几种实现

    近日项目开发中需要执行一些定时任务,比如需要在每天凌晨时候,分析一次前一天的日志信息,借此机会整理了一下定时任务的几种实现方式,由于项目采用spring框架,所以我都将结合 spring框架来介绍. ...

  9. Spring 定时任务2

    转载自http://www.cnblogs.com/nick-huang/p/4864737.html > 版本说明 <dependencies> <dependency> ...

随机推荐

  1. windows下修改pip安装源的办法

    之前的随笔里有写过关于Mac OS和Linux的,现在需要用到Windows的系统, 修改方法:路径----> C:\Users\用户名\AppData\Roaming,在Roaming文件夹下 ...

  2. CPU与内存

    一.存储单元内存有被划分为若干个存储单元,每个存储单元可存放1一个字节,即8个二进制位.因此内存的最小寻址单位是以字节进行的,每个存储单元都有一个编号.比如1GB的内存,可以储存1024*1024*1 ...

  3. flask部署:Ubuntu下使用nginx+uwsgi+supervisor部署flask应用

    之前一直用的Centos或者Red hat,自从使用Ubuntu后,发现Ubuntu使用起来更方便,自此爱上Ubuntu. 一.从github上下载flask应用 1.我已经成功将自己编写好的应用上传 ...

  4. MSDN原版系统镜像ISO下载站

    官网 网址1:http://www.imsdn.cn/ Windows 10 最新版本 1909 正式版 ISO 镜像下载 (微软 MSDN / VL 官方原版系统) 网址2:https://www. ...

  5. mongodb单机版的安装和配置

    首先根据操作系统下载MongoDB的相关安装文件 下载后上传到linux上,解压 将解压后的文件夹重新命名为monggodb4.0.1 命令mv 配置必要的文件夹mgdata存放运行数据,mglog存 ...

  6. Java文件处理之IO流

    一.概述 流:代表任何有能力产出数据的数据源对象或者是有能力接受数据的接收端对象 :其作用是为数据源和目的地建立一个输送通道. IO流:是程序中一套用于数据传输的机制.IO流是Input流和Outpu ...

  7. Node.js NPM 管理包

    章节 Node.js NPM 介绍 Node.js NPM 作用 Node.js NPM 包(Package) Node.js NPM 管理包 Node.js NPM Package.json 根据安 ...

  8. linux 命令之 objdump 简单使用

    objdump 介绍 objdump命令是用查看目标文件或者可执行的目标文件的构成的gcc工具 objdump 选项介绍 --archive-headers -a 显示档案库的成员信息,类似ls -l ...

  9. Dynamic Route Matching Vue路由(1)

    Dynamic Route Matching 动态的 路由 匹配 Very often we will need to map routes with the given pattern to the ...

  10. 动态添加,删除class样式

    function hasClass(obj, cls) { //class位于单词边界,判断class样式是否已经存在 return obj.className.match(new RegExp('( ...