1、需求應(yīng)用場景是這樣的: 使用Oracle數(shù)據(jù)保存待辦任務(wù),使用狀態(tài)字段區(qū)分任務(wù)是否已經(jīng)被執(zhí)行。多個Worker線程同時執(zhí)行任務(wù),執(zhí)行成功或失敗后,修改狀態(tài)字段的值。 假設(shè)數(shù)據(jù)庫表結(jié)構(gòu)如下所示。
flag 可取的值包括:0-待辦,1-已辦,-1-失敗待重試。 需要避免的問題: 2、分析2.1、依賴Java語言的機(jī)制Java語言的鎖機(jī)制可以解決并發(fā)問題,但只能在單機(jī)情況下有效。 在Tomcat(或其他應(yīng)用服務(wù)器)集群環(huán)境下,Java代碼中的鎖機(jī)制是解決不了這個問題的。 作為鎖的信號量,必須存儲在獨立于JVM的地方??梢允菙?shù)據(jù)庫,可以是Redis。 2.2、Quartz提供的支持在生產(chǎn)環(huán)境中,為了避免單點故障,Quartz需要集群提供 HA( High Availability,高可用)支持。Quartz集群依賴將任務(wù)信息持久化到數(shù)據(jù)庫中。 有兩個可選的思路: 1、可以設(shè)置單個節(jié)點的worker數(shù)量為1。首先保證了在單個節(jié)點內(nèi)不會有并發(fā)問題。是否能保證集群中同一個Job只有一個實例在跑,需要考察下 Quartz 提供的文檔。 2、在任務(wù)類上使用 @DisallowConcurrentExecution 或者 StatefullJob?;蛟S可以達(dá)到效果,需要實驗。 采用Quartz管理并發(fā)問題,采取的是回避策略,不能充分利用計算資源。 Quartz的集群環(huán)境依賴JDBC存儲,一方面需要通過數(shù)據(jù)庫在節(jié)點間共享信息,另一方面,基于數(shù)據(jù)庫的行集鎖解決了并發(fā)問題。 Quartz 本身已經(jīng)太龐大,不仔細(xì)閱讀文檔,甚至閱讀源代碼,也無從猜測其行為,作為企業(yè)級的 Timer ,它很好用。解決并發(fā)問題,還是找一個更清爽明了的方法吧。 2.3、通過數(shù)據(jù)庫鎖實現(xiàn)在考慮解決問題的方案前,先回顧一下數(shù)據(jù)庫的事務(wù)隔離級別和Oracle數(shù)據(jù)庫的鎖機(jī)制。 2.3.1、事務(wù)隔離級別事務(wù)隔離級別是針對當(dāng)前會話來說的。 SQL 92標(biāo)準(zhǔn)定義了4種事務(wù)隔離級別。 1、Read Uncommited : 可以讀到其他會話未提交到。 2、Read Commited :當(dāng)前會話可以讀到其他會話已經(jīng)提交到數(shù)據(jù)。
對于當(dāng)前會話來說,兩次讀取數(shù)據(jù),讀到的不一樣,這稱為“不可重復(fù)讀”。 這是Oracle默認(rèn)的事務(wù)隔離級別。 3、Repeatable Read :當(dāng)前會話看不到其他會話已經(jīng)提交的數(shù)據(jù)修改,但可以看到其他會話新插入的數(shù)據(jù)。 不可重復(fù)讀會出現(xiàn)的問題是:相同的查詢條件,在同一個會話中反復(fù)執(zhí)行,查詢得到記錄條數(shù)會不相同。這稱為“幻讀”。 4、Serializable :其他會話對數(shù)據(jù)的修改都不可見。 需要注意的是,不是其他會話不能修改數(shù)據(jù),而是修改對當(dāng)前會話不可見。 Oracle支持3種事務(wù)隔離級別。 Oracle支持Read Commited、Repeatable Read ,另外,支持 Read Only。 事務(wù)隔離級別可以幫我們理解問題,但不是解決問題的方法。 解決問題,靠數(shù)據(jù)庫的鎖機(jī)制。 2.3.2、Oracle數(shù)據(jù)庫的鎖機(jī)制我們需要的是DML鎖,DML鎖的目的在于保證并發(fā)情況下的數(shù)據(jù)完整性。在 Oracle 中,DML鎖包括表級鎖和行級鎖。 select … for update 可以獲得行級鎖。 這樣,我們可以有兩個方法達(dá)到并發(fā)控制的目的: 方法一: 通過select … for update 獲得行級鎖,鎖定若干任務(wù),每條數(shù)據(jù)是一個任務(wù)。 然后執(zhí)行任務(wù),執(zhí)行任務(wù)完成后,更新狀態(tài),提交事務(wù),釋放鎖。 Quartz 本身是使用這種機(jī)制,解決集群中的并發(fā)問題的。 相關(guān)代碼文件包括:
關(guān)鍵方法包括:obainLock,releseLock和executeInLock。 方法二: 按如下步驟執(zhí)行: 1、執(zhí)行如下SQL語句,搶占任務(wù)。
可以對如上SQL進(jìn)行改進(jìn),只搶占指定數(shù)量的任務(wù),多余的任務(wù)留給其他 worker 做。 2、逐個執(zhí)行已經(jīng)搶占的任務(wù)。 可以通過如下SQL查詢出已經(jīng)搶占成功的任務(wù)信息。
3、執(zhí)行完成后,更改任務(wù)狀態(tài)。
如果任務(wù)執(zhí)行失敗,放回去。
這種方法的要點在第一步。第一步是會出現(xiàn)并發(fā)問題的地方。 Oracle 的update語句會自動獲得行級鎖。我們可以做如下實驗驗證:
Oracle 本身對 update 的鎖機(jī)制已經(jīng)足以支持我們的工作。 方法三: 增加 version 或 timestamp 字段,使用樂觀鎖。 有了方法二,這種方法偏麻煩。 |
|