Quartz Task在Tomcat中重复运行

Posted by Night Field's Blog on June 9, 2020

问题描述

Spring Quartz是很常用的定时任务框架。把一个Quartz的工程部署到Tomcat中启动,意外地发现,每个Task都在同一时间跑了两次,而本地在开发的过程中却没有问题。

问题排查

为了防止多线程问题,有部分Task上是加了锁的,类似如下方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class ExampleTask{
	private ReentrantLock lock = new ReentrantLock();
    protected void executeInternal(){
    	if (lock.tryLock()) {
            try {
                // task main logic
            } finally {
                lock.unlock();
            }
        }
    }
}

按理说,SpringBean默认是单例的,加了锁之后,同一时间,只会有一个线程能拿到锁,然后执行Task的逻辑才对。难道锁不生效?于是我们又新增了类似如下日志,把ReentrantLock对象和this都打印出来:

1
logger.info("lock: " + lock + ", this: " + this);

得到:

1
2
2020-05-12 06:26:40 INFO  ExampleTask:30 - 7db46a61-e1e6-4d26-a038-d2f6721f70ac|lock: java.util.concurrent.locks.ReentrantLock@1cd8d32a[Unlocked], this: cn.com.nightfield.ExampleTask@121f2ec1
2020-05-12 06:26:40 INFO  ExampleTask:30 - 51afa06a-7d61-493c-943d-6e1f8c2ecc79|lock: java.util.concurrent.locks.ReentrantLock@7e7aab34[Unlocked], this: cn.com.nightfield.ExampleTask@70bd5a8b

表示震惊:ReentrantLockthis竟然都不是同一个实例! 于是我们大致可以有一个结论:应该是工程跑了两遍导致的。果然,在log中看到,QuartzScheduler被初始化了两次:

1
2
3
4
5
6
7
......
2020-05-12 06:26:23 INFO  QuartzScheduler:240 - Quartz Scheduler v.2.2.1 created.
2020-05-12 06:26:23 INFO  RAMJobStore:155 - RAMJobStore initialized.
......
2020-05-12 06:26:28 INFO  QuartzScheduler:240 - Quartz Scheduler v.2.2.1 created.
2020-05-12 06:26:28 INFO  RAMJobStore:155 - RAMJobStore initialized.
......

自然的,把目标放到了Tomcat身上。

检查了一下server.xml文件:

1
2
3
4
5
6
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
	<Context path="nightfield" docBase="/usr/local/tomcat/webapps/nightfield" debug="0" reloadable="false"/>
    <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
            prefix="localhost_access_log" suffix=".txt"
            pattern="%h %l %u %t &quot;%r&quot; %s %b" />
</Host>

问题就出在这里:我们把工程放到了Tomcatwebapps下面,而且把autoDeploy设成了true。 根据Tomcat官网对Automatic Application Deployment的介绍,当autoDeploytrue的时候,Tomcat会起线程监控appBase下的文件变化,当检测到有文件变化的时候,工程会被重新加载(reload)或被重新部署(redeploy)。所以在autoDeploy模式下,工程目录(docBase)需要指定在appBase目录之外:

When using automatic deployment, the docBase defined by an XML Context file should be outside of the appBase directory. If this is not the case, difficulties may be experienced deploying the web application or the application may be deployed twice. The deployIgnore attribute can be used to avoid this situation.

Note that if you are defining contexts explicitly in server.xml, you should probably turn off automatic application deployment or specify deployIgnore carefully. Otherwise, the web applications will each be deployed twice, and that may cause problems for the applications.

3. 问题解决

有了官网的指导,问题解决也就很简单了,有三种方法:

  1. 把工程放到webapps外面: ~~~xml
1
2
3
2. 把`appBase`设置成空:
~~~xml
<Host name="localhost" appBase="" unpackWARs="true" autoDeploy="true">
  1. autoDeploy设成false,顺便把deployOnStartup也设置成false ~~~xml
~~~ # 总结 一般情况下,`Tomcat`的`autoDeploy`功能在开发过程中很有用,能节省调试过程中重启服务的时间;但是在服务器环境上,推荐关闭此功能。不当的使用,可能会使服务多次部署,导致无法预料的bug。