0. 并發(fā)沖突的示例
單用戶的系統(tǒng)現(xiàn)在應(yīng)該比較罕見了,一般系統(tǒng)都會有很多用戶在同時進行操作;在多用戶系統(tǒng)中,涉及到的一個普遍問題:當多個用戶“同時”更新(修改或者刪除)同一條記錄時,該如何更新呢?
下圖展示了開放式并發(fā)沖突的一個示例:
假設(shè)數(shù)據(jù)庫中有一條記錄Record{Field1=5, Field2=6, Field3=7}(以下簡寫為{5, 6, 7}),A、B兩個用戶按照如下順序操作這一條記錄:
(1). A讀取該記錄,取得的值為{5, 6, 7},讀取完畢后,不對該記錄加排他鎖;
(2). B讀取該記錄,取得的值也為{5, 6, 7},讀取完畢后,不對該記錄加排他鎖;
(3). B將該記錄修改為{3, 5, 7},并寫回數(shù)據(jù)庫;由于該記錄沒有被其他用戶鎖定,且B在修改時該記錄的值與第(2)步中讀取的值一致,因此可以正常寫入數(shù)據(jù)庫;
(4). A將該記錄修改為{5, 8, 9},并寫回數(shù)據(jù)庫;由于A在修改時該記錄的值已更新為{3, 5, 7},與第(1)步中讀取的值{5, 6, 7}不一致,因此引發(fā)并發(fā)沖突;
1. 開放式并發(fā)(樂觀并發(fā)) & 封閉式并發(fā)(悲觀并發(fā))
開始之前,先介紹兩個概念(From 《SQL Server 2008聯(lián)機叢書》)。
| 樂觀并發(fā)控制 | 在樂觀并發(fā)控制中,用戶讀取數(shù)據(jù)時不鎖定數(shù)據(jù)。當一個用戶更新數(shù)據(jù)時,系統(tǒng)將進行檢查,查看該用戶讀取數(shù)據(jù)后其他用戶是否又更改了該數(shù)據(jù)。如果其他用戶更新了數(shù)據(jù),將產(chǎn)生一個錯誤。一般情況下,收到錯誤信息的用戶將回滾事務(wù)并重新開始。這種方法之所以稱為樂觀并發(fā)控制,是由于它主要在以下環(huán)境中使用:數(shù)據(jù)爭用不大且偶爾回滾事務(wù)的成本低于讀取數(shù)據(jù)時鎖定數(shù)據(jù)的成本。 |
| 悲觀并發(fā)控制 | 一個鎖定系統(tǒng),可以阻止用戶以影響其他用戶的方式修改數(shù)據(jù)。如果用戶執(zhí)行的操作導致應(yīng)用了某個鎖,只有這個鎖的所有者釋放該鎖,其他用戶才能執(zhí)行與該鎖沖突的操作。這種方法之所以稱為悲觀并發(fā)控制,是因為它主要用于數(shù)據(jù)爭用激烈的環(huán)境中,以及發(fā)生并發(fā)沖突時用鎖保護數(shù)據(jù)的成本低于回滾事務(wù)的成本的環(huán)境中。 |
舉個例子來說:
樂觀并發(fā):本文起始位置的圖片,展示的是樂觀并發(fā)情況下引發(fā)的沖突。
悲觀并發(fā):繼續(xù)沿用圖片中示例的請求順序,如果第(1)步中A讀取這條記錄后,給記錄加上排他鎖,并且一直持有,在更新完畢之前不釋放該鎖;則第(2)步中B嘗試讀取該記錄時,請求會被阻塞,直到A釋放該記錄,或者請求超時。因此新的執(zhí)行順序(假設(shè)請求沒有超時)為:A讀取并鎖定記錄{5, 6, 7}-->B嘗試讀取該記錄[B被阻塞等待]-->A寫回{5, 8, 9}到數(shù)據(jù)庫,并釋放該記錄-->B讀取到{5, 8, 9},并鎖定該記錄-->B修改該記錄并寫回數(shù)據(jù)庫……
2. Linq to SQL中的樂觀并發(fā)控制
L2S支持開放式并發(fā)控制。使用L2S執(zhí)行修改和刪除操作時,同時打開SQL Server Profile來查看生成的SQL代碼,我們可以看到類似這樣的代碼(假設(shè)表上加了TimeStamp字段):
UPDATE TableName SET Field1=@p0, Field2=@p1 WHERE PrimaryKey=@p2 AND TimeStampField=@p3
//(省略)....
DELETE TableName WHERE PrimaryKey=@p0 AND TimeStampField=@p1
//(省略)....
當記錄被更新時,則其TimeStamp字段會被自動更新。因此,如果在用戶讀取該記錄后、且更新該記錄之前,有其他用戶更新過這條記錄,則更新會失敗(根據(jù)受影響行數(shù)為0來判斷),L2S會拋出ChangeConflictException異常。
以上描述的是表上有加timeStamp字段的情況,如果表上沒有TimeStamp字段,L2S會對映射為UpdateCheck = UpdateCheck.Always 或 UpdateCheck.WhenChanged的字段成員進行開放式并發(fā)檢查,可以根據(jù)Sql server Profile來查看,不再贅述。
3. 沖突解決
既然有了沖突,就需要把沖突給和諧掉。
還是以本文起始位置的例子來說,最后A更新時,該更新為啥呢?可以存在如下三種選擇:
(1). 覆蓋數(shù)據(jù)值庫:{5,8,9}?
(2). 保留數(shù)據(jù)庫值:{3,5,7}?
(3). 合并為:{3,8,9}?
這三種方式分別對了L2S的三種解決方案:
3.1 通過覆蓋數(shù)據(jù)庫值解決并發(fā)沖突
try{ db.SubmitChanges(ConflictMode.ContinueOnConflict); //需要指定為ConflictMode.ContinueOnConflict}
catch (ChangeConflictException e){foreach (ObjectChangeConflict occ in db.ChangeConflicts)
{ occ.Resolve(RefreshMode.KeepCurrentValues); //保留當前值,覆蓋數(shù)據(jù)庫中的值}
}
db.SubmitChanges(ConflictMode.FailOnFirstConflict); //處理完沖突后,重試3.2 通過保留數(shù)據(jù)庫值解決并發(fā)沖突
try{db.SubmitChanges(ConflictMode.ContinueOnConflict);
}
catch (ChangeConflictException e){foreach (ObjectChangeConflict occ in db.ChangeConflicts)
{ occ.Resolve(RefreshMode.OverwriteCurrentValues);//以數(shù)據(jù)庫中的值,重寫當前值}
}
db.SubmitChanges(ConflictMode.FailOnFirstConflict); //處理完沖突后,重試
3.3 通過與數(shù)據(jù)庫值合并解決并發(fā)沖突
try{db.SubmitChanges(ConflictMode.ContinueOnConflict);
}
catch (ChangeConflictException e){foreach (ObjectChangeConflict occ in db.ChangeConflicts)
{ occ.Resolve(RefreshMode.KeepChanges);//保留數(shù)據(jù)庫中的值和當前值,進行合并處理}
}
db.SubmitChanges(ConflictMode.FailOnFirstConflict); //處理完沖突后,重試
浙公網(wǎng)安備 33010602011771號