最近項目中使用spring+quartz的方式來實現(xiàn)跑批任務,偶然發(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,默認的事務隔離級別是rr,在框架中用來維護scheduler的類是ClusterManager,看報錯信息是這個類的clusterCheckIn方法執(zhí)行的時候發(fā)生死鎖。
- 解析下clusterCheckIn()干什么了?
先從它的上一層方法說doCheckin(),關鍵代碼。
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如果返回結果為0條,那就insert這條記錄。死鎖就是發(fā)生在這個地方。
當在debug的時候,我先提交一個update語句,但是這條語句并未命中,在rr事務隔離級別中這會觸發(fā)gap鎖,防止其他事務中進行插入。這時其他線程(例如其他節(jié)點)同樣會執(zhí)行這段代碼,先提交update語句,同樣觸發(fā)gap鎖,gap與gap相互兼容,這時無論哪個線程提交insert語句之后都會阻塞,當?shù)诙€線程提交insert之后就會發(fā)生死鎖,事務1希望事務2釋放gap讓自己完成insert操作,事務2希望事務1釋放gap讓自己完成insert操作。
我個人覺得這個問題quartz框架并沒有解決,只是在正常情況下不容易出現(xiàn),這個bug在1.8.4中被提出過(bug鏈接) 查看源碼發(fā)現(xiàn)解決的辦法就是再執(zhí)行過程中進行了一次commit。
failedRecords = clusterCheckIn(conn);
commitConnection(conn);
- 解決辦法
將數(shù)據(jù)庫的默認隔離級別修改成rc,不存在gap鎖,就不會出現(xiàn)這種問題。
另外我們這個情況是因為在開發(fā)環(huán)境debug,導致線程全部阻塞,增加了發(fā)生死鎖的概率,最后把debug級別修改成thread后就沒在發(fā)生過.
以上是粗讀quartz-scheduler源碼總結,如果有錯誤的地方歡迎指點。