在 Spring 中动态管理定时任务,通过简单的一句自动注入 ThreadPoolTaskScheduler 对象的代码,即可轻松实现,参见 Spring动态管理定时任务——ThreadPoolTaskScheduler 。
一、问题抛出
但如果没有查看 ThreadPoolTaskScheduler 的源码,则要特别注意 ThreadPoolTaskScheduler 中,初始化 poolSize=1。源码如下:
@SuppressWarnings("serial")
public class ThreadPoolTaskScheduler extends ExecutorConfigurationSupport
implements AsyncListenableTaskExecutor, SchedulingTaskExecutor, TaskScheduler {
private volatile int poolSize = 1;
....
}
所以将会导致多个任务串行方式执行,而如果前面的某个任务一直没执行结束(比如不限制次数的重试机制),则会使得后面的任务一直没有机会执行。
二、问题复现
复现日志1(正常情况):
【结论一】:可以看到4个(corn=0 0 8 * * ?)的定时任务串行执行。
上面的日志里没打印线程名(log4j中用%t代表),如果打印出来,则将是同一线程。
jconsole查看线程名称:
也可验证 ThreadPoolTaskScheduler 默认 poolSize 只有1,所以上面的4个任务是串行执行。
复现日志2(异常情况):
【结论二】:任务7执行成功,紧接着轮到任务9,而我的业务要求每个任务失败必须一直重试,导致后面的任务没有机会执行。
三、解决方案
3.1 设置poolSize固定值
设置 poolSize为固定值5,如下:
/**
* 数据同步定时任务调度器
*/
@Bean(autowire = Autowire.BY_NAME, name = "sync")
public ThreadPoolTaskScheduler threadPoolTaskScheduler4Sync() {
ThreadPoolTaskScheduler syncScheduler = new ThreadPoolTaskScheduler();
syncScheduler.setPoolSize(5);
syncScheduler.setThreadGroupName("syncTg");
syncScheduler.setThreadNamePrefix("syncThread-");
return syncScheduler;
}
@Resource(name = "sync")
private ThreadPoolTaskScheduler threadPoolTaskScheduler;
注意,这里设置以后,Spring 帮我们通过 IOC 实例化好了一个 ThreadPoolTaskScheduler 对象,poolSize 属性为5,但是通过jconsole发现并没有创建出 5 个工作线程,通过调试发现,其内置的executor线程池 poolSize 为 0!
我们开启多于 5 个任务来验证一下,jconsole 再次查看线程名称和个数为固定值 5:
3.2 动态设置poolSize值
然而,实际场景中往往不能固定设置死 poolSize 的值。特别是对任务调度系统来说,当需要同时运行一些各自独立的毫无关联的任务时,就会受到固定值的限制,造成的结果就是,只有池子里的任务有执行结束后,池子之外的任务才有机会被加入执行。更糟的情况是,当池子里的任务都在因为异常或业务要求(比如出错无限重试)而导致池子永远无法得到释放,将导致固定值之外的任务永远不会被执行!
所以,我们要么初始化 poolSize 为一个相对大的数值,要么我们必须动态去改变 ThreadPoolTaskScheduler 的 poolSize 大小。显然,后者更合理且灵活一些,翻看 ThreadPoolTaskScheduler.setPoolSize(int) 的方法注释:
>>>> “This setting can be modified at runtime, for example through JMX.”
从而得知允许我们在运行时动态调整 poolSize 大小。
3.3.1 业务代码调整
具体做法是,在启动停止任务的 Controller 方法中,通过比较当前 poolSize 大小和期望值来调整。
3.3.2 Restful 接口调整
为 poolSize 大小的管理创建和实现一些 restful 接口,当需要调整时,通过接口调用的方式完成。
3.3.3 JMX 调整
主要步骤如下:
- 创建调整 poolSize 的接口 ThreadPoolTaskSchedulerMBean
- 创建一个类 ThreadPoolTaskSchedulerWrapper,实现step1中的接口,且设置成员变量为 ThreadPoolTaskScheduler 对象
- 注册 ThreadPoolTaskSchedulerWrapper 对象到 MBean 服务器
下面给出一个 JMX 操作或管理 MBean 的例子:
(1)创建操作接口,
public interface UserMBean {
String getName();
void SetName(String name);
String getPasswd();
void SetPasswd(String pwd);
int add(int x, int y);
}
(2)实现接口,并添加一些成员变量,
public class User implements UserMBean {
private String name;
private String passwd;
@Override
public String getName() {
return name;
}
@Override
public void SetName(String name) {
this.name = name;
}
@Override
public String getPasswd() {
return passwd;
}
@Override
public void SetPasswd(String pwd) {
this.passwd = pwd;
}
@Override
public int add(int x, int y) {
return x + y;
}
}
(3)注册MBean,
import java.lang.management.ManagementFactory;
import javax.management.InstanceAlreadyExistsException;
import javax.management.MBeanRegistrationException;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.NotCompliantMBeanException;
import javax.management.ObjectName;
public class TestMBean {
public static void main(String[] args) throws MalformedObjectNameException, InstanceAlreadyExistsException,
MBeanRegistrationException, NotCompliantMBeanException, InterruptedException {
MBeanServer server = ManagementFactory.getPlatformMBeanServer();
ObjectName objName = new ObjectName("jmx:type=User");
User bean = new User();
server.registerMBean(bean, objName);
System.out.println("JMX started..");
String oldName = null;
String oldPwd = null;
while (true) {
if (oldName != bean.getName() || oldPwd != bean.getPasswd()) {
System.out.println(bean.getName() + bean.getPasswd());
oldName = bean.getName();
oldPwd = bean.getPasswd();
}
Thread.sleep(1000L);
}
}
}
最后启动程序后,打开 jconsole, 再打开 MBean 标签页,找到注册的 MBean 对象User,执行操作调用即可。
以上。