最近項目中使用spring+quartz
的方式來實現(xiàn)跑批任務(wù),偶然發(fā)現(xiàn)日志中存在InnoDB中deadlock,排查一番真是廢了很多精力。
- 先上一炮異常:
2016-08-15 14:51:06.136 [QuartzScheduler_scheduler-WilliamLee1471243617837_ClusterManager] ERROR jdbc.sqlonly - 109. PreparedStatement.executeUpdate() INSERT INTO QRTZ_SCHEDULER_STATE (SCHED_NAME, INSTANCE_NAME, LAST_CHECKIN_TIME, CHECKIN_INTERVAL) VALUES('scheduler', 'WilliamLee1471243617837', 1471243866096, 10000)
com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
---
blablabla
---
at org.quartz.impl.jdbcjobstore.JobStoreSupport.clusterCheckIn(JobStoreSupport.java:3401) [quartz-2.2.1.jar:na]
at org.quartz.impl.jdbcjobstore.JobStoreSupport.doCheckin(JobStoreSupport.java:3253) [quartz-2.2.1.jar:na]
at org.quartz.impl.jdbcjobstore.JobStoreSupport$ClusterManager.manage(JobStoreSupport.java:3858) [quartz-2.2.1.jar:na]
at org.quartz.impl.jdbcjobstore.JobStoreSupport$ClusterManager.run(JobStoreSupport.java:3895) [quartz-2.2.1.jar:na]
quartz-scheduler
中集群模式是通過數(shù)據(jù)庫來交互的,我們使用的是mysql
,默認(rèn)的事務(wù)隔離級別是rr
,在框架中用來維護(hù)scheduler的類是ClusterManager
,看報錯信息是這個類的clusterCheckIn方法執(zhí)行的時候發(fā)生死鎖。
- 解析下clusterCheckIn()干什么了?
先從它的上一層方法說doCheckin(),關(guān)鍵代碼。
Connection conn = getNonManagedTXConnection();
if (!firstCheckIn) {
failedRecords = clusterCheckIn(conn);
commitConnection(conn);
}
failedRecords = (firstCheckIn) ? clusterCheckIn(conn) : findFailedInstances(conn);
clusterRecover(conn, failedRecords);
首先拿到一個autocommit=false
的Connection,然后執(zhí)行clusterCheckIn()
,返回failedRecords
,最后會在clusterRecover()
刪除掉這些failedRecords
。接下來看看clusterCheckIn()
。
lastCheckin = System.currentTimeMillis();
if(getDelegate().updateSchedulerState(conn, getInstanceId(), lastCheckin) == 0) {
getDelegate().insertSchedulerState(conn, getInstanceId(), lastCheckin, getClusterCheckinInterval());
}
先update
如果返回結(jié)果為0條,那就insert
這條記錄。死鎖就是發(fā)生在這個地方。
當(dāng)在debug
的時候,我先提交一個update
語句,但是這條語句并未命中,在rr
事務(wù)隔離級別中這會觸發(fā)gap鎖,防止其他事務(wù)中進(jìn)行插入。這時其他線程(例如其他節(jié)點)同樣會執(zhí)行這段代碼,先提交update
語句,同樣觸發(fā)gap鎖,gap與gap相互兼容,這時無論哪個線程提交insert
語句之后都會阻塞,當(dāng)?shù)诙€線程提交insert
之后就會發(fā)生死鎖,事務(wù)1希望事務(wù)2釋放gap讓自己完成insert操作,事務(wù)2希望事務(wù)1釋放gap讓自己完成insert操作。
我個人覺得這個問題quartz框架并沒有解決,只是在正常情況下不容易出現(xiàn),這個bug在1.8.4中被提出過(bug鏈接) 查看源碼發(fā)現(xiàn)解決的辦法就是再執(zhí)行過程中進(jìn)行了一次commit。
failedRecords = clusterCheckIn(conn);
commitConnection(conn);
- 解決辦法
將數(shù)據(jù)庫的默認(rèn)隔離級別修改成rc
,不存在gap鎖,就不會出現(xiàn)這種問題。
另外我們這個情況是因為在開發(fā)環(huán)境debug,導(dǎo)致線程全部阻塞,增加了發(fā)生死鎖的概率,最后把debug級別修改成thread后就沒在發(fā)生過.
以上是粗讀quartz-scheduler源碼總結(jié),如果有錯誤的地方歡迎指點。