並發分為兩種,一種叫做悲觀並發,一種叫樂觀並發。 名字挺文藝
悲觀並發
我們先看一下不控制並發時的場景
之所以能捕捉到錯誤 是因為 EF這里的操作機制是將 被Timestamp標識的字段加入 where子句。
那如果捕捉到了異常,EF會怎么處理呢?使用Reload處理
針對單個字段的並發
悲觀並發
悲觀並發是什么呢? 就拿我們常用的代碼版本控制來說。 有一個文檔,A和B都要 獲取這個文檔並進行修改, 如果當A在讀取這個文檔數據時,就單獨霸占了這個文檔,B無法獲取,只有當A讀取修改完畢釋放鎖時,B才能獲取這個文件,也就是一個人去的控制權的時候,其他人只能等待。這就是悲觀鎖。因為擔心,多人同時操作造成的數據紊亂,大概是因為建立在這樣的心態上,取名 “悲觀鎖”的。
悲觀鎖 通常用在頻繁發生數據競爭的激烈環境下,以及 通過鎖來保護數據所需的成本小於回滾事務的成本的時候。
樂觀並發
樂觀並發顯然,“心理”上是反過來的,就是 “我不擔心A和B同時取得控制權,A和B可以同時讀取 同時修改,但是A修改保存后,B再修改保存,這時候系統會發現,當前文檔和B進入系統時不一樣了,就會報錯。這顯然也是一種不錯的處理方式。
樂觀鎖 應用場景一般是 數據競爭並不是特別激烈,且 偶爾的數據爭執所需的回滾事務的成本小於讀取數據時鎖定數據所需要的成本。樂觀並發的初衷在於,不希望經常性的看到數據爭執。
悲觀並發如果加鎖的成本較高的話,會很明顯的降低效率,降低系統的並發性。
樂觀並發 通常分為 三個階段 讀取階段--校驗階段--寫入階段
在讀取階段,A和B分別將數據讀入各自機器緩沖,此時並沒有校驗,在校驗階段,系統事務會對文件進行同步校驗,如果不出現問題,則進入第三階段寫入,數據最終被提交,否則報錯。
悲觀鎖還有一個常見的問題就是“死鎖”,比如文檔T1 和T2是內容相關的,但是不巧 T1被A鎖住了 T2有被B鎖住了, 那就會卡在這直到有一方先取消鎖。
我們先看一下不控制並發時的場景
//未進行並發處理 User user = new User { UserName="shenwei" ,certID= "11111"}; using (BlogContext ctx= new BlogContext()) { ctx.Users.Add(user); ctx.SaveChanges(); } //首先插入一條數據 並提交 //定義兩個context同時進行操作 BlogContext firContext = new BlogContext (); User u1 = firContext.Users.FirstOrDefault(); BlogContext secContext = new BlogContext (); User u2 = secContext.Users.FirstOrDefault(); u2.UserName = "zhangxiaomao" ; //改變名字 並提交 secContext.SaveChanges(); u1.UserName = "xxxxxx" ; u1.certID = "22222" ; //另一個操作改變certid,也提交 firContext.SaveChanges();
數據庫 查詢
select
*
from
Users
;

回到我們的EF codefirst . EntityFramework只支持樂觀並發,也就是說EF其實並不希望經常性的看到數據沖突。
針對整條記錄的並發
EF實現並發控制 需要借助 TimeStamp 標示 ,並且一個類只能有 一個此標示,標示的必須是byte[]類型
public class Blog { public string ID { get; set; } public string BlogName { get; set; } public string BlogAuthor { get; set; } public virtual List <Post> Posts { get; set ; } //導航屬性 public BlogDetails Detail { get; set; } [ Timestamp] public byte [] version { get; set; } }
測試如下
//並發模擬 Blog b = new Blog { ID = "24", BlogName = "Gaea", BlogAuthor = "shenwei", Detail = new BlogDetails { } }; //先通過一個ctx插入數據並提交 using(BlogContext context=new BlogContext()) { context.Blogs.Add(b); context.SaveChanges(); } //創建一個ctx取的第一條數據,修改 但是不提交 BlogContext fircontext = new BlogContext(); Blog firstblog = fircontext.Blogs.FirstOrDefault(); firstblog.BlogName = "哈哈,被改掉了" ; //創建另一個ctx還是取第一條數據,修改並提交 BlogContext secContext = new BlogContext(); Blog secondBlog = secContext.Blogs.FirstOrDefault(); secondBlog.BlogAuthor = "JasonShen"; secContext.SaveChanges(); //這個時候再提交第一個ctx所做的修改 try { //這是后會發現現在的數據,已經和剛進入時發生了變化,故報錯 fircontext.SaveChanges(); Console.WriteLine("保存成功" ); } catch(Exception e) { Console.WriteLine("保存失敗" ); } Console.ReadKey(); }
結果如下

select
*
from
Blogs
;

之所以能捕捉到錯誤 是因為 EF這里的操作機制是將 被Timestamp標識的字段加入 where子句。
一開始插入一條數據之后,時間戳是這樣的,初始版本的對象也就是這個樣子

后來兩個context 各自獲取這個對象,其中一個進行修改,並提交,這個時候數據庫中的時間戳標示的字段已經發生了改變。

這個時候 ,另一個context提交的時候 執行update .... where version=‘初始版本的version’ 然后會發現找不到,於是就報錯!
也就是說依靠 timespan標示的字段來確認是否與初始版本發生了改動,若發生了,就報錯,進行錯誤處理。
那如果捕捉到了異常,EF會怎么處理呢?使用Reload處理
Resolving optimistic concurrency exceptions with Reload
使用Reload數據作為解決樂觀並發異常的策略之一,除了Reload外,還有其他幾種沖突解決策略,這里只講下常用的Reload
微軟Entity Framework 團隊 推薦處理樂觀並發沖突的策略之一是Reload數據,也就是EF檢測到並發沖突時會拋出Db
updateConcurrencyException,這時解決沖突分為Client Wins或者Store Wins ,而Reload處理也就是Store Wins,意味着放棄當前內存中的實體,重新到數據庫中加載當前實體,EF官方團隊給出來的示例代碼如下
捕捉到異常的時候
try { //這是后會發現現在的數據,已經和剛進入時發生了變化,故報錯 fircontext.SaveChanges(); Console .WriteLine("保存成功" ); } catch (DbUpdateConcurrencyException e) { Console .WriteLine("保存失敗" ); Console .WriteLine("Reload" ); e.Entries.Single().Reload(); Console .WriteLine(firstblog.BlogName); //會發現 變成了初始從數據庫里加載的數據值 }
針對單個字段的並發
有些時候並不需要控制針對整條記錄的並發,只需要控制某個列的數據 不會出現臟操作就ok
這個時候 就使用
ConcurrencyCheck 標示
public class User { [Key ,DatabaseGenerated (DatabaseGeneratedOption .Identity)] public Guid UserGuid { get; set; } public string UserName { get; set; } [ ConcurrencyCheck ] public string certID { get; set; } }
//針對單個字段 標示的ConcurrencyCheck 的並發 User user = new User { UserName = "shenwei" , certID = "11111" }; using (BlogContext ctx = new BlogContext ()) { ctx.Users.Add(user); ctx.SaveChanges(); } //首先插入一條數據 並提交 //定義兩個context同時進行操作 BlogContext firContext = new BlogContext (); User u1 = firContext.Users.FirstOrDefault(); BlogContext secContext = new BlogContext (); User u2 = secContext.Users.FirstOrDefault(); u2.certID= "22222" ; //改變名字 並提交 secContext.SaveChanges(); try { u1.certID = "33333" ; //另一個操作改變certid,也提交 firContext.SaveChanges(); } catch (Exception e) { Console .WriteLine("並發報錯" ); }
當然可以同時用concurrentCheck標示多個字段,那被標示的每個都不能被同時修改了。這里背后的機制同樣是where將被標示的字段作為了篩選條件。
總結
經過分析樂觀鎖 並不適合處理高並發的場景,少量的數據沖突才是樂觀並發的初衷。 悲觀鎖同樣也不適合處理高並發,特別在加鎖成本比較大的時候。
如果項目並發量確實大, 那就可以考慮采用其他技術實現,比如 消息隊列等。
如果您喜歡這篇文章,歡迎推薦!