要專業系統地學習EF推薦《你必須掌握的Entity Framework 6.x與Core 2.0》。這本書作者(汪鵬,Jeffcky)的博客:https://www.cnblogs.com/CreateMyself/
來到並發這里了,我自己得先承認,並發對我來說完全是一個熟悉又真正陌生的東西,總的來說,我對並發一無所知。
那么不管是怎么回事,我也要說一下。之前看過零星的一些講硬件的東西,說的是,很多個應用你看似同時開啟,同時運行的,其實只是,CPU速度太快,讓你察覺不了。
所以不可能存在兩個任務同時進行,這只是錯覺。所以我現在給自己一些自信,我斷定!不存在,就像一個啤酒瓶,口就那么大,一次只容許一顆珠子進去,不可能兩個同時進去,都是錯覺!
來看EF中的並發。
我們在使用EF上下文時,遵循的是一個請求對應一個上下文,對事務也是這個態度,不要事務那么長,越短越好。
一個請求對應一個上下文,那么服務器同時接受到了多個請求,構造出多個上下文對象,針對同一資源操作,問題就出來這里。
因為不同的上下文中查詢出的實體都是各自的,並不是同一個引用。
這里有兩個上下文,都得到了名叫“張三”的學生實體,第一個上下文修改為“李四”,第二個上下文修改為“張三”,那么最終的結果應該是“張三”,但是看看下面的代碼,其實最終數據庫的結果是“李四”
using (DB1_Context ctx1 = new DB1_Context()) using (DB1_Context ctx2 = new DB1_Context()) { var stu1 = ctx1.Students.FirstOrDefault(); var stu2 = ctx2.Students.FirstOrDefault(); stu1.Name = "李四"; stu2.Name = "張三"; ctx1.SaveChanges(); ctx2.SaveChanges(); }
你覺得應該是第一個上文查詢修改完,再第二個上下文接着查詢修改就行了。但是高並發的情況下是無法保證的。
那么我們看下一個上下文中查詢相同的兩個實體。引用是相等的。所以整個解決方案就使用一個上下文是不是就行了?我覺得是,但是這是不科學的。
using (DB1_Context ctx = new DB1_Context()) { var stu1 = ctx.Students.FirstOrDefault(); var stu2 = ctx.Students.FirstOrDefault(); Console.WriteLine(ReferenceEquals(stu1, stu2)); // True Console.WriteLine($"stu1.Name:{stu1.Name},stu2.Name:{stu2.Name}"); // stu1.Name:小新77,stu2.Name:小新77 }
並發沖突做個分初級、中級和高級來講,我這篇筆記主要記錄初級內容的學習心得。
現在來認識一下悲觀並發和樂觀並發,這是兩種並發的控制方法
悲觀並發:當更新特定記錄時,同一記錄上的所有其他並發更新將被阻塞,直到當前操作完成或者放棄,其他並發操作才可以繼續。
樂觀並發:當更新特定記錄時,同一記錄上的所有其他並發將導致最后一條記錄被保存(獲勝)。假設由於並發訪問共享資源而導致資源沖突並不是不可能過的,而是不可用的,此時將采取一定手段來解決並發沖突。
上面的張三李四就是屬於樂觀並發,就是我就隨他去了,它自己修改到哪里就是哪里,我也不關心過程。
那么如何解決上面的問題,上面是什么問題?就是我第二個上下文查詢出實體不是最新的,應該將這種情況看做是一種異常,但是如果你用Try/catch來捕獲是捕獲不到的。
因為捕獲並發沖突需要特殊配置,EF就為我們提供了兩種方式:並發Token、行版本(RowVersion)
如果我們對student的Name屬性這是並發Token,需要將屬性進行如下配置
modelBuilder.Entity<Student>().Property(x => x.Name).IsConcurrencyToken();
現在來用try/catch就可以捕獲了
System.Data.Entity.Infrastructure.DbUpdateConcurrencyException: Store update, insert, or delete statement affected an unexpected number of rows (0).
Entities may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=472540 for information on understanding and handling optimistic concurrency exceptions.
---> System.Data.Entity.Core.OptimisticConcurrencyException: Store update, insert, or delete statement affected an unexpected number of rows (0).
Entities may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=472540 for information on understanding and handling optimistic concurrency exceptions.
這個異常我之前沒有學到這里來的時候碰到過,沒有記錄下來當時是寫的什么代碼,真可惜!
來看看行版本的方式。這就需要為實體添加一個字節數組類型的屬性,並且該屬性需要配置
public byte[] RowVersion { get; set; }
modelBuilder.Entity<Student>().Property(x => x.RowVersion).IsRowVersion();
在數據庫中就是這樣的,每次數據更新時,數據庫中的RowVersion也會如時間戳一樣得到更新,從而檢測數據庫中所存儲的值與實體中的值是否一致來檢測並發沖突。
那么接下來我們就開始在異常處理中進行操作,他不是數據不是最新的嗎?那么我就讓他得到最新的。因為EF中有針對並發異常的類(DbUpdateConcurrencyException)。
DbUpdateConcurrencyException中具有Entries屬性,該屬性返回一系列DbEntityEntry對象,表示沖突實體的跟蹤信息。
using (DB1_Context ctx1 = new DB1_Context()) using (DB1_Context ctx2 = new DB1_Context()) { var stu1 = ctx1.Students.FirstOrDefault(); var stu2 = ctx2.Students.FirstOrDefault(); stu1.Name = "小新111"; ctx1.SaveChanges(); stu2.Name = "小新222"; try { ctx2.SaveChanges(); } catch (DbUpdateConcurrencyException ex) { var s = ex.Entries.Single(); s.Reload(); Console.WriteLine("stu2.Name:" + stu2.Name); // 小新11 stu2.Name = "小新222"; ctx2.SaveChanges(); throw ex; } }
調用Reload方法來刷新數據庫中的最新值到當前內存中的值,就是造成並發沖突的這個對象,更新它。
如果說不用Relod,也有另外一種方式來實現
using (DB1_Context ctx1 = new DB1_Context()) using (DB1_Context ctx2 = new DB1_Context()) { //ctx1.Database.Log = msg => Console.WriteLine("ctx11111111111111:" + msg); //ctx2.Database.Log = msg => Console.WriteLine("ctx22222222222222:" + msg); var stu1 = ctx1.Students.FirstOrDefault(); var stu2 = ctx2.Students.FirstOrDefault(); stu1.Name = "小新11"; ctx1.SaveChanges(); stu2.Name = "小新22"; try { ctx2.SaveChanges(); } catch (DbUpdateConcurrencyException ex) { // 獲取並發異常被追蹤的實體 var tracking = ex.Entries.Single(); // 獲取數據庫原始值對象,數據庫中沒被修改之前的值 var original = (Student)tracking.OriginalValues.ToObject(); Console.WriteLine(original.Name); // 小新 // 獲取更新后數據庫最新的值對象,就是數據庫中目前的值,這一句會發起查詢 var database = (Student)tracking.GetDatabaseValues().ToObject(); Console.WriteLine(database.Name); // 小新11 // 獲取當前內存的值,就是造成並發異常的值 var current = (Student)tracking.CurrentValues.ToObject(); Console.WriteLine(current.Name); // 小新22 tracking.OriginalValues.SetValues(database); //tracking.GetDatabaseValues().SetValues(current); ctx2.SaveChanges(); // 需要調用savechanges方法 throw ex; } }
這里有一個疑問,照我的理解應該是將current的值賦值給當前數據庫中的值,也就是tracking.GetDatabaseValues().SetValues(current);
但是這樣寫報錯,雖然作者也專門解釋了,但是我還是懵的……
行吧,這個還是必要自己去動手弄一下,體會一下。初級版的並發沖突解決方案就到這里了。
后面還是不得不說一下,我也是今天才知道多個using可以這個很簡單的堆疊起來寫,很優雅啊。
然后利用上下文的日志打印真的很有用。
using (DB1_Context ctx1 = new DB1_Context()) using (DB1_Context ctx2 = new DB1_Context()) { ctx1.Database.Log = msg => Console.WriteLine("ctx111111111111111:" + msg); ctx2.Database.Log = msg => Console.WriteLine("ctx222222222222222:" + msg); var stu1 = ctx1.Students.FirstOrDefault(); var stu2 = ctx2.Students.FirstOrDefault(); stu1.Name = "小新11"; stu2.Name = "小新22"; ctx1.SaveChanges(); ctx2.SaveChanges(); }
從打印的結果可以看到,關於數據庫初始化的任務全部是由ctx1去執行的,就是這些什么Migration這些東西
難道是我ctx1對象先構造的問題?或者ctx1的log先打印的問題,於是我改成ctx2先構造,然后ctx2的log也先執行,發現還是上面打印的結果,還是ctx1去執行數據庫初始化的工作。
直到我將ctx2先查詢出student對象才變成ctx2先執行這些操作。所以是不是就認識到,多個上下文到底是誰來負責數據庫初始化的任務呢?那就看看是誰先與數據庫交互了,現在構造上下文對象這里並沒有與數據庫發生交互。
行吧,就這了,后面還會繼續學習。