問題描述
Spring Quartz是很常用的定時任務框架。把一個Quartz
的工程部署到Tomcat
中啓動,意外地發現,每個Task
都在同一時間跑了兩次,而本地在開發的過程中卻沒有問題。
問題排查
爲了防止多線程問題,有部分Task
上是加了鎖的,類似如下方式:
@Component
public class ExampleTask{
private ReentrantLock lock = new ReentrantLock();
protected void executeInternal(){
if (lock.tryLock()) {
try {
// task main logic
} finally {
lock.unlock();
}
}
}
}
按理說,Spring
中Bean
默認是單例的,加了鎖之後,同一時間,只會有一個線程能拿到鎖,然後執行Task
的邏輯纔對。難道鎖不生效?於是我們又新增了類似如下日誌,把ReentrantLock
對象和this
都打印出來:
logger.info("lock: " + lock + ", this: " + this);
得到:
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
表示震驚:ReentrantLock
和this
竟然都不是同一個實例!
於是我們大致可以有一個結論:應該是工程跑了兩遍導致的。果然,在log中看到,QuartzScheduler
被初始化了兩次:
......
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
文件:
<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 "%r" %s %b" />
</Host>
問題就出在這裏:我們把工程放到了Tomcat
的webapps
下面,而且把autoDeploy
設成了true。
根據Tomcat
官網對Automatic Application Deployment的介紹,當autoDeploy
是true的時候,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. 問題解決
有了官網的指導,問題解決也就很簡單了,有三種方法:
- 把工程放到
webapps
外面:
<Context path="nightfield" docBase="/usr/local/nightfield" debug="0" reloadable="false"/>
- 把
appBase
設置成空:
<Host name="localhost" appBase="" unpackWARs="true" autoDeploy="true">
- 把
autoDeploy
設成false,順便把deployOnStartup
也設置成false
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="false" deployOnStartup="false">
總結
一般情況下,Tomcat
的autoDeploy
功能在開發過程中很有用,能節省調試過程中重啓服務的時間;但是在服務器環境上,推薦關閉此功能。不當的使用,可能會使服務多次部署,導致無法預料的bug。