場(chǎng)景
在某一個(gè)時(shí)間段赠堵,系統(tǒng)有定時(shí)任務(wù)會(huì)對(duì)某一張表的數(shù)據(jù)進(jìn)行處理小渊,在處理的這時(shí)候可能會(huì)有其他的服務(wù)會(huì)來對(duì)這個(gè)表進(jìn)行操作。如果他們操作了同一個(gè)行記錄茫叭,這個(gè)時(shí)間點(diǎn)上就會(huì)出現(xiàn)數(shù)據(jù)不一致的可能性酬屉。所以需要對(duì)這種情況進(jìn)行處理。
解決方案
背景介紹:任務(wù)當(dāng)前是對(duì)表數(shù)據(jù)進(jìn)行遍歷取出來揍愁,然后逐條進(jìn)行執(zhí)行呐萨。
方案一
這種方案是最簡(jiǎn)單,改造成本最低的莽囤。我們可以加一個(gè)變量來標(biāo)識(shí)任務(wù)是否有在執(zhí)行的過程中谬擦,執(zhí)行的過程中拒絕其他服務(wù)的調(diào)用即可。這里就不展開贅述了朽缎。
方案二
這個(gè)方案的設(shè)計(jì)初衷就是標(biāo)題所述惨远,想在定時(shí)任務(wù)的執(zhí)行過程中蔚舀,其他服務(wù)也可以進(jìn)行調(diào)用。但是其他服務(wù)要先來定時(shí)任務(wù)處進(jìn)行查詢是否已被處理锨络,未被執(zhí)行到的行記錄就更新下狀態(tài)赌躺,讓定時(shí)任務(wù)跳過當(dāng)條記錄,已處理的數(shù)據(jù)則拒絕更新羡儿,來保證數(shù)據(jù)的一致性礼患。
定時(shí)任務(wù)的流程圖大概是這樣的
1.加載資源處理成map
2.遍歷每一條記錄
3.判斷該記錄是否被其他服務(wù)更新狀態(tài),是則跳過掠归,否則繼續(xù)處理
st=>start: 開始
op1=>operation: 處理資源成map
op2=>operation: 遍歷執(zhí)行每一條記錄(判斷狀態(tài))
cond1=>condition: 該條記錄是否被更新狀態(tài)
op3=>operation: 繼續(xù)處理該條記錄
e=>end: 結(jié)束
st->op1->op2->cond1->op3->e
cond1(no)->op3
cond1(yes)->op2
改造思路
定時(shí)任務(wù)部分
- 將原有的list存儲(chǔ)換成ConcurrentHashMap以支持多線程的操作缅叠,結(jié)構(gòu)以主鍵作為key,value是處理的表對(duì)象 + 該條記錄的狀態(tài)(0-未處理虏冻, 1-處理中肤粱, 2-已處理)。
- 將封裝的ConcurrentHashMap對(duì)外暴露以給其他接口服務(wù)進(jìn)行調(diào)用厨相。
- 遍歷的時(shí)候领曼,對(duì)該條記錄的狀態(tài)進(jìn)行判斷,對(duì)狀態(tài)被更新為2的記錄進(jìn)行不處理蛮穿。
ps:這里狀態(tài)為2的記錄就是被其他服務(wù)修改的庶骄,然后通知給定時(shí)任務(wù)。
其他服務(wù)接口
- 查詢這個(gè)ConcurrentHashMap是否當(dāng)前記錄在定時(shí)任務(wù)的處理中践磅,不在則可以直接使用单刁,在則對(duì)狀態(tài)進(jìn)行更新為2,通知定時(shí)任務(wù)不可以繼續(xù)處理了府适。
存在的問題
這種方案設(shè)計(jì)完成后羔飞,我深思了一下。發(fā)現(xiàn)無(wú)法保證同時(shí)間同一個(gè)key在同時(shí)get和put的時(shí)候執(zhí)行的順序檐春,所以會(huì)存在這種邊界問題(我自己這么稱之)逻淌。雖然ConcurrentHashMap是線程安全的,但是它任然無(wú)法滿足我的要求喇聊,如下偽代碼所示恍风。
//其他服務(wù)接口
line = map.get(key); //這里先取到
if(line.state == 0){ //滿足未執(zhí)行的時(shí)候蹦狂,進(jìn)行更新
//更新state=2;
}
在get和put執(zhí)行的區(qū)間誓篱,定時(shí)任務(wù)同時(shí)也進(jìn)行了該條行記錄的數(shù)據(jù),這就會(huì)產(chǎn)生數(shù)據(jù)不一致的問題凯楔。
方案二其實(shí)無(wú)法保證數(shù)據(jù)的一致性窜骄,在稍微極端的情況下。于是就有了方案三摆屯,基于方案二的迭代思路邻遏。
方案三
在方案二中我們知道了極端情況下糠亩,數(shù)據(jù)會(huì)存在不一致的問題。是因?yàn)閿?shù)據(jù)在第一次被其他服務(wù)接口讀取后准验,又在定時(shí)任務(wù)的循環(huán)中被讀取了赎线,重新進(jìn)行處理。那么我們對(duì)這里進(jìn)行一定的改造是不是就可以了糊饱。順著這個(gè)思路我就繼續(xù)往下想垂寥。
然后還真被我想到了處理方法,也是我最近在學(xué)習(xí)spring源碼的過程中得到到想法另锋,我可以對(duì)被讀取的數(shù)據(jù)進(jìn)行封裝滞项,不直接是行記錄的對(duì)象,正如spring-beans中的bean和beanWrapper的關(guān)系**
封裝類偽代碼如下夭坪。
public class LineWrapper<T> {
private T line; //封裝的行對(duì)象
private volatile int state = 0; //計(jì)數(shù)器
private ReentrantLock lock = new ReentrantLock(); //鎖
public LineWrapper(T line){
this.line = line;
}
public T getLine(){
//state != 0則表表明不是第一次就不用進(jìn)行鎖的判斷可以直接返回文判,如果第一次則進(jìn)行嘗試加鎖
if(state == 0 && lock.tryLock()){
lock.lock(); //加鎖
state++; //增加計(jì)數(shù)器
return line; //返回存在的對(duì)象
}
return null; //否則返回null,這樣不管是定時(shí)任務(wù)還是其他服務(wù)接口都不進(jìn)行處理
//TODO 在finally進(jìn)行鎖的釋放
}
}
這樣設(shè)計(jì)之后就可以保證在極端的情況下也不會(huì)出現(xiàn)該行記錄被操作兩次室梅,同時(shí)也可以拋棄掉ConcurrentHashMap戏仓,使用HashMap,來提高一些性能亡鼠。
Ps
方案三默認(rèn)是單機(jī)版存儲(chǔ)在內(nèi)存中的
如果在微服務(wù)的架構(gòu)下柜去,可以使用 redis + 分布式鎖 來進(jìn)行替換
最終
finally,選取了第一種方案(好失落安鹜稹)嗓奢,雖然第三種方案更加的完善,支持同時(shí)的處理浑厚。但是它的改造成本要高于第一種方案不少股耽,同時(shí)復(fù)雜性提升也會(huì)帶來更多可能出現(xiàn)的問題。這里雖然只選對(duì)的不選擇最好的道理钳幅,但是我們同樣也不要放棄思考更優(yōu)的解決方案物蝙。
如果有不對(duì)的,也請(qǐng)多多指正敢艰。畢竟一家之見诬乞,多有偏頗。
謝謝