一、背景
lock 確保當一個線程位於代碼的臨界區時,另一個線程不進入臨界區。如果其他線程試圖進入鎖定的代碼,則它將一直等待(即被阻止),直到該對象被釋放。
二、悲觀並發和樂觀並發
悲觀並發:比如有兩個用戶A,B,同時登錄系統修改一個文檔,如果A先進入修改,則系統會把該文檔鎖住,B就沒辦法打開了,只有等A修改完,完全退出的時候B才能進入修改。
樂觀並發:同上面的例子,A,B兩個用戶同時登錄,如果A先進入修改緊跟着B也進入了。A修改文檔的同時B也在修改。如果在A保存之后B再保存他的修改,此時系統檢測到數據庫中文檔記錄與B剛進入時不一致,B保存時會拋出異常,修改失敗。
樂觀並發的基本出發點是:當保存數據的時候抱着一種樂觀的態度,不期望發生並發沖突,即使萬一發生並發沖突,也能捕捉到沖突異常,然后根據策略解決沖突,而解決沖突的方式一般分為Client wins(以后操作者為贏) 和 Store wins(以先存儲的數據為贏)。
三、EF中如何控制並發

Resolving optimistic concurrency exceptions with Reload
使用Reload數據作為解決樂觀並發異常的策略之一,我在這里就講數據Reload這一種策略就好了,除了Reload外,還有其他幾種沖突解決策略,詳見參考文獻中EF官方團隊博客。
using (var context = new UnicornsContext()) { bool saveFailed; do { saveFailed = false; var unicorn = context.Unicorns.Find(1); unicorn.Name = "tom"; try { context.SaveChanges(); } catch (DbUpdateConcurrencyException ex) { saveFailed = true; // Update the values of the entity that failed to save // from the store ex.Entries.Single().Reload(); } } while (saveFailed); }
四、SqlProfiler看原理
完成以上步驟后,您的程序就具備了樂觀並發處理的能力了,但是,其中的原理是什么呢?從SqlProfiler監控結果來看,EF是將並發模式設置為Fixed的字段放在where子句里,后操作會因為取不到相應的記錄而更新失敗,打個比方說,當兩個用戶同時對同一條記錄(ID=1,Count=10)進行讀、寫操作的時候,A、B同時讀取記錄的時候,Count都等於10,A用戶先進行Update減1操作,此時(ID=1,Count=9),而此時B用戶稍微晚一點點再進行Update操作的時候,因為Count已經被A修改成9了,已經不存在(ID=1,Count=10)的記錄了,所以B最終執行影響行數為0,EntityFramework拋出並發異常:
如:我們給Count屬性的並發模式設置成Fixed的話,那生成的SQL語句如下:
exec sp_executesql N'update [dbo].[OrderLog] set [Count] = @0 where (([ID] = @1) and ([Count] = @2))',N'@0 int,@1 int,@2 int',@0=78,@1=1,@2=9
當EntityFramework執行更新操如果影響行數為0,就會拋出異常,相關源代碼如下所示:


五、總結
個人認為,樂觀並發控制適用於並發量還不是很大情況,也就是符合樂觀並發的初衷,當保存數據的時候不期望發生並發沖突,一旦發生沖突,也能捕捉到沖突異常,然后根據策略解決沖突或者提示用戶操作失敗等,但是,當並發量很大的時候,我認為樂觀並發就顯得並不那么適用了,個人建議,做評估項目並發風險和做並發沖突的測試,假如完全可以接受,大可以應用EntityFramework的樂觀並發控制,實現起來也比較簡單,假如項目並發量確實很大,那可以考慮別的技術方案實現,比如消息隊列……等。
參考文獻
(1)微軟EntityFramework團隊博客: Using DbContext in EF 4.1 Part 9: Optimistic Concurrency Patterns
(2)Gyoung: Entity Framework 並發處理