原網站:C# Entity Framework併發處理 在軟體開發過程中,併發控制是確保及時糾正由併發操作導致的錯誤的一種機制。從 ADO.NET 到 LINQ to SQL 再到如今的 ADO.NET Entity Framework,.NET 都為併發控制提供好良好的支持方案。併發處理方式一般分 ...
在軟體開發過程中,併發控制是確保及時糾正由併發操作導致的錯誤的一種機制。從 ADO.NET 到 LINQ to SQL 再到如今的 ADO.NET Entity Framework,.NET 都為併發控制提供好良好的支持方案。
併發處理方式一般分為樂觀必併發與悲觀必併發兩種,本文將為大家介紹 Entity Framework 、 LINQ to SQL 中的併發處理方式。在本文最後,將提供一個了可參考的方案,結合事務與併發控制確保全全的數據交換機制。
目錄
一、併發處理的定義
二、模型屬性的併發處理選項
三、Entity Framewrok 悲觀併發
四、Entity Framework 樂觀併發
五、回顧 LINQ to SQL 併發處理的方式
六、結合事務處理併發衝突
一、併發處理的定義
在軟體開發過程中,當多個用戶同時修改一條數據記錄時,系統需要預先制定對併發的處理模式。併發處理模式主要分為兩種:
第一種模式稱為悲觀式併發,即當一個用戶已經在修改某條記錄時,系統將拒絕其他用戶同時修改此記錄。
第二種模式稱為樂觀式併發,即系統允許多個用戶同時修改同一條記錄,系統會預先定義由數據併發所引起的併發異常處理模式,去處理修改後可能發生的衝突。常用的樂觀性併發處理方法有以下幾種:
- 保留最後修改的值。
- 保留最初修改的值。
- 合併多次修改的值。
相對於LINQ TO SQL 中的併發處理方式,Entity Framework 中的併發處理方式實現了不少的簡化,下麵為大家一一介紹。
二、模型屬性的併發處理選項
在System.Data.Metadata.Edm 命名空間中,存在ConcurencyMode 枚舉,用於指定概念模型中的屬性的併發選項。ConcurencyMode有兩個成員
成員名稱 | 說明 |
None | 在寫入時從不驗證此屬性。 這是預設的併發模式。 |
Fixed | 在寫入時始終驗證此屬性。 |
當模型屬性為預設值 None 時,系統不會對此模型屬性進行檢測,當同一個時間對此屬性進行修改時,系統會以數據合併方式處理輸入的屬性值。
當模型屬性為Fixed 時,系統會對此模型屬性進行檢測,當同一個時間對屬性進行修改時,系統就會激發OptimisticConcurrencyException 異常。
開發人員可以為對象的每個屬性定義不同的 ConcurencyMode 選項,選項可以在*.csdl 找看到:
1 <Schema> 2 ...... 3 ...... 4 <EntityType Name="Person"> 5 <Key> 6 <PropertyRef Name="Id" /> 7 </Key> 8 <Property Type="Int32" Name="Id" Nullable="false" annotation:StoreGeneratedPattern="Identity" /> 9 <Property Type="String" Name="FirstName" MaxLength="50" FixedLength="false" Unicode="true" 10 ConcurrencyMode="Fixed" /> 11 <Property Type="String" Name="SecondName" MaxLength="50" FixedLength="false" Unicode="true" /> 12 <Property Type="Int32" Name="Age" /> 13 <Property Type="String" Name="Address" MaxLength="50" FixedLength="false" Unicode="true" /> 14 <Property Type="String" Name="Telephone" MaxLength="50" FixedLength="false" Unicode="true" /> 15 <Property Type="String" Name="EMail" MaxLength="50" FixedLength="false" Unicode="true" /> 16 </EntityType> 17 </Schema>
三、Entity Framework 悲觀併發
在一般的開發過程中,最常用的是悲觀併發處理。.NET 提供了Lock、Monitor、Interlocked 等多個鎖定數據的方式,它可以保證同一個表裡的對象不會同時被多個客戶進行修改,避免了系統數據出現邏輯性的錯誤。
由於本篇文章主要介紹併發處理方式,關於鎖的介紹,請參考http://www.cnblogs.com/leslies2/archive/2012/02/08/2320914.html#t8
1 public int Update(Person person) 2 { 3 int n = -1; 4 try 5 { 6 using (BusinessEntities context = new BusinessEntities()) 7 { 8 lock (this) 9 { 10 var obj = context.Person.Where(x => x.Id == person.Id).First(); 11 if (obj != null) 12 context.ApplyCurrentValues("Person", person); 13 n = context.SaveChanges(); 14 } 15 } 16 } 17 catch (Exception ex) 18 { ...... } 19 return n; 20 }
使用悲觀併發雖然能有效避免數據發生邏輯性的錯誤,但使用 lock 等方式鎖定 Update 方法的操作,在用戶同時更新同一數據表的數據,操作就會被延時或禁止。在千萬級 PV 的大型網路系統當中使用悲觀併發,有可能降低了系統的效率,此時可以考慮使用樂觀併發處理。
四、Entity Framework 樂觀併發
為瞭解決悲觀併發所帶來的問題,ADO.NET Entity Framework 提供了更為高效的樂觀併發處理方式。相對於LINT to SQL , ADO.NET Entity Framework 簡化了樂觀併發的處理方式,它可以靈活使用合併數據、保留初次輸入數據、保留最新輸入數據等方式處理併發衝突。
4.1 以合併方式處理併發數據
當模型屬性的 ConcurencyMode 為預設值 None ,一旦同一個對象屬性同時被修改,系統將以合併數據的方式處理併發衝突,這也是 Entity Framework 處理併發衝突的預設方式。合併處理方式如下:當同一時間針對同一個對象屬性作出修改,系統將保存最新輸入的屬性值。當同一時間對同一對象的不同屬性作出修改,系統將保存已被修改的屬性值。下麵用兩個例子作出說明:
4.1.1 同時更新數據
在系統輸入下麵代碼,先獲取資料庫中的 Id 為24的對象 Person,使用Display方法在數據修改前顯示對象的最初值。然後使用非同步方法分兩次調用Update方法,同時更新Person對象的相關屬性,第一次更新對象的 FirstName 屬性,第二次更新對象的 SecondName、Age 兩個屬性。最後,在使用SaveChanges保存更新後,顯示數據更新後的信息。
1 public class PersonDAL 2 { 3 public Person GetPerson(int id) 4 { 5 using (BusinessEntities context = new BusinessEntities()) 6 { 7 IQueryable<Person> list=context.Person.Where(x => x.Id == id); 8 return list.First(); 9 } 10 } 11 12 public void Update(Person person) 13 { 14 using (BusinessEntities context = new BusinessEntities()) 15 { 16 //顯示輸入新數據的信息 17 Display("Current", person); 18 var obj = context.Person.Where(x => x.Id == person.Id).First(); 19 if (obj != null) 20 context.ApplyCurrentValues("Person", person); 21 22 //虛擬操作,保證數據能同時加入到上下文當中 23 Thread.Sleep(100); 24 context.SaveChanges(); 25 } 26 } 27 28 delegate void MyDelegate(Person person); 29 30 public static void Main(string[] args) 31 { 32 //在更新數據前顯示對象信息 33 PersonDAL personDAL = new PersonDAL(); 34 var beforeObj = personDAL.GetPerson(24); 35 personDAL.Display("Before", beforeObj); 36 37 //更新Person的SecondName,Age兩個屬性 38 Person person1 = new Person(); 39 person1.Id = 24; 40 person1.FirstName = "Leslie"; 41 person1.SecondName = "Wang"; 42 person1.Age = 32; 43 person1.Address = "Tianhe"; 44 person1.Telephone = "13660123456"; 45 person1.EMail = "[email protected]"; 46 47 //更新Person的FirstName屬性 48 Person person2 = new Person(); 49 person2.Id = 24; 50 person2.FirstName = "Rose"; 51 person2.SecondName = "Lee"; 52 person2.Age = 34; 53 person2.Address = "Tianhe"; 54 person2.Telephone = "13660123456"; 55 person2.EMail = "[email protected]"; 56 57 //使用非同步方式同時更新數據 58 MyDelegate myDelegate = new MyDelegate(personDAL.Update); 59 myDelegate.BeginInvoke(person1, null, null); 60 myDelegate.BeginInvoke(person2, null, null); 61 62 Thread.Sleep(300); 63 //在更新數據後顯示對象信息 64 var afterObj = personDAL.GetPerson(24); 65 personDAL.Display("After", afterObj); 66 Console.ReadKey(); 67 } 68 69 public void Display(string message,Person person) 70 { 71 String data = string.Format("{0}\n Person Message:\n Id:{1} FirstName:{2} "+ 72 "SecondName:{3} Age:{4}\n Address:{5} Telephone:{6} EMail:{7}\n", 73 message, person.Id, person.FirstName, person.SecondName, person.Age, 74 person.Address, person.Telephone, person.EMail); 75 Console.WriteLine(data); 76 } 77 }
根據操作結果可以看到,在Entity Framework的預設環境情況下,系統會使用合併方式處理併發,把輸入數據的所有修改值都保存到當前上下文當中,並同時修改資料庫當中的值。
4.1.2 刪除與更新操作同時運行
Entity Framework 能以完善的機制靈活處理同時更新同一對象的操作,但一旦刪除操作與更新操作同時運行時,就可能存在邏輯性的異常。例如:兩個客戶端同時載入了同一個對象,第一個客戶端更新了數據後,把數據再次提交。但在提交前,第二個客戶端已經把資料庫中的已有數據刪除。此時,上下文中的對象處於不同的狀態底下,將會引發 OptimisticConcurrencyException 異常。
遇到此異常時,可以用 try(OptimisticConcurrencyException){...} catch {...} 方式捕獲異常,然後使用 ObjectStateManager.ChangeObjectState 方法更改對象的 EntityState 屬性。把EntityState 更改為 Added ,被刪除的數據便會被再次載入。若把 EntityState 更改為 Detached 時,數據便會被順利刪除。下麵把對象的 EntityState 屬性更改為 Added 作為例子。
1 public class PersonDAL 2 { 3 delegate int MyDelegate(Person person); 4 5 public static void Main(string[] args) 6 { 7 //在更新數據前顯示對象信息 8 PersonDAL personDAL = new PersonDAL(); 9 var beforeObj = personDAL.GetPerson(51); 10 personDAL.DisplayProperty("Begin", beforeObj); 11 12 //更新Person的屬性 13 Person person1 = new Person(); 14 person1.Id = 51; 15 person1.FirstName = "Leslie"; 16 person1.SecondName = "Wang"; 17 person1.Age = 32; 18 person1.Address = "Tianhe"; 19 person1.Telephone = "13660123456"; 20 person1.EMail = "[email protected]"; 21 22 //使用非同步方式更新數據 23 MyDelegate myDelegate = new MyDelegate(personDAL.Update); 24 IAsyncResult reslut=myDelegate.BeginInvoke(person1, null, null); 25 26 //同步刪除原有數據 27 personDAL.Delete(51); 28 //顯示刪除後重新被載入的數據 29 var afterObj = personDAL.GetPerson(myDelegate.EndInvoke(reslut)); 30 personDAL.DisplayProperty("End", afterObj); 31 } 32 33 public Person GetPerson(int id) 34 { 35 using (BusinessEntities context = new BusinessEntities()) 36 { 37 IQueryable<Person> list=context.Person.Where(x => x.Id == id); 38 return list.First(); 39 } 40 } 41 42 //更新對象 43 public int Update(Person person) 44 { 45 int returnValue=-1; 46 using (BusinessEntities context = new BusinessEntities()) 47 { 48 var obj = context.Person.Where(x => x.Id == person.Id).First(); 49 //顯示對象所處狀態 50 DisplayState("Before Update", obj); 51 try 52 { 53 if (obj != null) 54 context.ApplyCurrentValues("Person", person); 55 //虛擬操作,保證數據已經在資料庫中被非同步刪除 56 Thread.Sleep(100); 57 context.SaveChanges(); 58 returnValue = obj.Id; 59 } 60 catch (System.Data.OptimisticConcurrencyException ex) 61 { 62 //把對象的狀態更改為 Added 63 context.ObjectStateManager.ChangeObjectState(obj, System.Data.EntityState.Added); 64 context.SaveChanges(); 65 returnValue=obj.Id; 66 } 67 } 68 return returnValue; 69 } 70 71 //刪除對象 72 public void Delete(int id) 73 { 74 using (BusinessEntities context = new BusinessEntities()) 75 { 76 var person1 = context.Person.Where(x => x.Id == id).First(); 77 if (person1 != null) 78 context.Person.DeleteObject(person1); 79 context.SaveChanges(); 80 //顯示對象現在所處的狀態 81 DisplayState("After Delete:", person1); 82 } 83 } 84 85 //顯示對象現在所處的狀態 86 public void DisplayState(string message,Person person) 87 { 88 String data = string.Format("{0}\n Person State:{1}\n", 89 message,person.EntityState); 90 Console.WriteLine(data); 91 } 92 //顯示對象相關屬性 93 public void DisplayProperty(string message, Person person) 94 { 95 String data = string.Format("{0}\n Person Message:\n Id:{1} FirstName:{2} " + 96 "SecondName:{3} Age:{4}\n Address:{5} Telephone:{6} EMail:{7}\n", 97 message, person.Id, person.FirstName, person.SecondName, person.Age, 98 person.Address, person.Telephone, person.EMail); 99 Console.WriteLine(data); 100 } 101 }
觀察運行測試結果,當運行 Delete 方法,對象已經在資料庫中被刪除,對象的EntityState處於 Detached 狀態。此時使用 SaveChanges 保存更新數據時,引發了OptimisticConcurrencyException 異常。在捕獲異常,把對象狀態更改為 Added ,再使用SaveChanges保存數據,數據就能順利地保存到資料庫中。但值得留意,因為對象是在刪除後重新載入的,所以對象的 Id 也會被同步更新。
以合併數據的方式處理併發衝突固然方便快節,但在業務邏輯較為複雜的系統下並不適合使用此處理方式。比如在常見的Order、OrderItem的表格中,OrderItem 的單價,數量會直接影響Order的總體價格,這樣使用合併數據的方式處理併發,有可能引起邏輯性的錯誤。此時,應該考慮以其他方式處理併發衝突。
4.2 當發生數據併發時,保留最新輸入的數據
要驗證輸入對象的屬性,必須先把該屬性的 ConcurencyMode 設置為 Fixed,這樣系統就會實時檢測對象屬性的輸入值 。
當該屬性被同時更新,系統便會激發 OptimisticConcurrencyException 異常。捕獲該異常後,可以使用 ObjectContext.Refresh (RefreshMode,object) 刷新上下文中該對象的狀態,當 RefreshMode 為 ClientWins 時,系統將會保持上下文中的現在有數據,即保留最新輸入的對象值。此時再使用ObjectContext.SaveChanges, 系統就會把最新輸入的對象值加入資料庫當中。
在下麵的例子當,系統啟動前先把 Person 的 FirstName、SecondName 兩個屬性的 ConcurencyMode 屬性設置為Fixed,使系統能監視這兩個屬性的更改。所輸入的數據只在FirstName、SecondName 兩個值中作出修改。在數據提交前先以 DisplayProperty 方法顯示資料庫最初的數據屬性,在數據初次更新後再次調用 DisplayProperty 顯示更新後的數據屬性。在第二次更新數據時,將引發OptimisticConcurrencyException 異常,此時把引發異常的對象屬性再次顯示出來。對異常進行處理後,顯示資料庫中最終的對象值。
1 public class PersonDAL 2 { 3 delegate void MyDelegate(Person person); 4 5 public static void Main(string[] args) 6 { 7 //在更新數據前顯示對象信息 8 PersonDAL personDAL = new PersonDAL(); 9 var beforeObj = personDAL.GetPerson(52); 10 personDAL.DisplayProperty("Before", beforeObj); 11 12 //更新Person的FirstName、SecondName屬性 13 Person person1 = new Person(); 14 person1.Id = 52; 15 person1.FirstName = "Mike"; 16 person1.SecondName = "Wang"; 17 person1.Age = 32; 18 person1.Address = "Tianhe"; 19 person1.Telephone = "13660123456"; 20 person1.EMail = "[email protected]"; 21 22 //更新Person的FirstName、SecondName屬性 23 Person person2 = new Person(); 24 person2.Id = 52; 25 person2.FirstName = "Rose"; 26 person2.SecondName = "Chen"; 27 person2.Age = 32; 28 person2.Address = "Tianhe"; 29 person2.Telephone = "13660123456"; 30 person2.EMail = "[email protected]"; 31 32 //使用非同步方式更新數據 33 MyDelegate myDelegate = new MyDelegate(personDAL.Update); 34 myDelegate.BeginInvoke(person1, null, null); 35 myDelegate.BeginInvoke(person2, null, null); 36 //顯示完成更新後數據源中的對應屬性 37 Thread.Sleep(1000); 38 var afterObj = personDAL.GetPerson(52); 39 personDAL.DisplayProperty("After", afterObj); 40 } 41 42 public Person GetPerson(int id) 43 { 44 using (BusinessEntities context = new BusinessEntities()) 45 { 46 IQueryable<Person> list=context.Person.Where(x => x.Id == id); 47 return list.First(); 48 } 49 } 50 51 //更新對象 52 public void Update(Person person) 53 { 54 using (BusinessEntities context = new BusinessEntities()) 55 { 56 var obj = context.Person.Where(x => x.Id == person.Id).First(); 57 try 58 { 59 if (obj!=null) 60 context.ApplyCurrentValues("Person", person); 61 //虛擬操作,保證數據被同步載入 62 Thread.Sleep(100); 63 context.SaveChanges(); 64 //顯示第一次更新後的數據屬性 65 this.DisplayProperty("Current", person); 66 } 67 catch (System.Data.OptimisticConcurrencyException ex) 68 { 69 //顯示發生OptimisticConcurrencyException異常所輸入的數據屬性 70 this.DisplayProperty("OptimisticConcurrencyException", person); 71 72 if (person.EntityKey == null) 73 person.EntityKey = new System.Data.EntityKey("BusinessEntities.Person", 74 "Id", person.Id); 75 //保持上下文當中對象的現有屬性 76 context.Refresh(RefreshMode.ClientWins, person); 77 context.SaveChanges(); 78 } 79 } 80 } 81 82 //顯示對象相關屬性 83 public void DisplayProperty(string message, Person person) 84 { 85 String data = string.Format("{0}\n Person Message:\n Id:{1} FirstName:{2} " + 86 "SecondName:{3} Age:{4}\n Address:{5} Telephone:{6} EMail:{7}\n", 87 message, person.Id, person.FirstName, person.SecondName, person.Age, 88 person.Address, person.Telephone, person.EMail); 89 Console.WriteLine(data); 90 } 91 }
觀察測試結果,可見當RefreshMode狀態為ClientWins時,系統將會保存上下文當中的對象屬性,使用此方法可以在發生併發異常時保持最新輸入的對象屬性。
4.3 當發生數據併發時,保留最初輸入的數據
把對象屬性的 ConcurencyMode 設置為 Fixed 後,同時更新該屬性,將會激發 OptimisticConcurrencyException 異常。此時使用 ObjectContext.Refresh (RefreshMode,object) 刷新上下文中該對象的狀態,當 RefreshMode 為 StoreWins 時,系統就會把數據源中的數據代替上下文中的數據。
因為初次調用 SaveChanges,數據可以成功保存到資料庫。但是在 ObjectContext 並未釋放時,再次使用 SaveChanges 非同步更新數據,就會引發OptimisticConcurrencyException 併發異常。當 RefreshMode 為 StoreWins 時,系統就會保留初次輸入的數據屬性。
此例子與上面的例子十分相似,只是把 RefreshMode 改為 StoreWins 而已。在業務邏輯較為複雜的的系統當中,建議使用此方式處理併發異常。在保留最初輸入的數據修改屬性後,把屬性返還給客戶,讓客戶進行對比後再決定下一步的處理方式。
1 public class PersonDAL 2 { 3 delegate void MyDelegate(Person person); 4 5 public static void Main(string[] args) 6 { 7 //在更新數據前顯示對象信息 8 PersonDAL personDAL = new PersonDAL(); 9 var beforeObj = personDAL.GetPerson(52); 10 personDAL.DisplayProperty("Before", beforeObj); 11 12 //更新Person的FirstName、SecondName屬性 13 Person person1 = new Person(); 14 person1.Id = 52; 15 person1.FirstName = "Mike"; 16 person1.SecondName = "Wang"; 17 person1.Age = 32; 18 person1.Address = "Tianhe"; 19 person1.Telephone = "13660123456"; 20 person1.EMail = "[email protected]"; 21 22 //更新Person的FirstName、SecondName屬性 23 Person person2 = new Person(); 24 person2.Id = 52; 25 person2.FirstName = "Rose"; 26 person2.SecondName = "Chen"; 27 person2.Age = 32; 28 person2.Address = "Tianhe"; 29 person2.Telephone = "13660123456"; 30 person2.EMail = "[email protected]"; 31 32 //使用非同步方式更新數據 33 MyDelegate myDelegate = new MyDelegate(personDAL.Update); 34 myDelegate.BeginInvoke(person1, null, null); 35 myDelegate.BeginInvoke(person2, null, null); 36 //顯示完成更新後數據源中的對應屬性 37 Thread.Sleep(1000); 38 var afterObj = personDAL.GetPerson(52); 39 personDAL.DisplayProperty("After", afterObj); 40 } 41 42 public Person GetPerson(int id) 43 { 44 using (BusinessEntities context = new BusinessEntities()) 45 { 46 IQueryable<Person> list=context.Person.Where(x => x.Id == id); 47 return list.First(); 48 } 49 } 50 51 //更新對象 52 public void Update(Person person) 53 { 54 using (BusinessEntities context = new BusinessEntities()) 55 { 56 var obj = context.Person.Where(x => x.Id == person.Id).First(); 57 try 58 { 59 if (obj!=null) 60 context.ApplyCurrentValues("Person", person); 61 //虛擬操作,保證數據被同步載入 62 Thread.Sleep(100); 63 context.SaveChanges(); 64 //顯示第一次更新後的數據屬性 65 this.DisplayProperty("Current", person); 66 } 67 catch (System.Data.OptimisticConcurrencyException ex) 68 { 69 //顯示發生OptimisticConcurrencyException異常所輸入的數據屬性 70 this.DisplayProperty("OptimisticConcurrencyException", person); 71 72 if (person.EntityKey == null) 73 person.EntityKey = new System.Data.EntityKey("BusinessEntities.Person", 74 "Id", person.Id); 75 //保持數據源中對象的現有屬性 76 context.Refresh(RefreshMode.StoreWins, person); 77 context.SaveChanges(); 78 } 79 } 80 } 81 82 //顯示對象相關屬性 83 public void DisplayProperty(string message, Person person) 84 { 85 String data = string.Format("{0}\n Person Message:\n Id:{1} FirstName:{2} " + 86 "SecondName:{3} Age:{4}\n Address:{5} Telephone:{6} EMail:{7}\n", 87 message, person.Id, person.FirstName, person.SecondName, person.Age, 88 person.Address, person.Telephone, person.EMail); 89 Console.WriteLine(data); 90 } 91 }
觀察測試結果,可見當 RefreshMode 狀態為 StoreWins 時,系統將會以數據源中的數據代替上下文當中的對象屬性。在業務邏輯較為複雜的的系統當中,建議使用此方式處理併發異常。
回到目錄
五、回顧 LINQ to SQL 併發處理的方式
Entity Framework 當中簡化了併發處理的方式,然而溫故而知新,LINQ to SQL 中併發處理所使用的方式也值得回顧一下。下麵將與大家一起回顧一下 LINQ to SQL 當中併發處理的方式。
與 Entity Framework 相似,LINQ to SQL 中表格的每個列都為可以設定不同處理方式,屬性 UpdateCheck (更新檢查) 的預設值為 Always,即系統會在預設情況下檢查屬性的併發狀態。若把屬性改為 WhenChanged,即當該屬性發生改變時,系統才會對其進行檢測。若把屬性改為 Nerver , 這時系統將不會對此屬性進行檢查,總是接受最新一次的輸入值。
處理 LINQ to SQL 併發,最為重要的是以下兩個方法:
DataContext.SubmitChanges(ConflictMode)
DataContext.ChangeConflicts.ResolveAll(RefreshMode);
SubmitChanges 將對檢索到的對象所做的更改發送到基礎資料庫,並通過 ConflictMode 指定併發衝突時要採取的操作 。當選擇 ConflictMode.FailOnFirstConflict 時,若檢測到第一個併發衝突錯誤時,系統會立即停止對更新資料庫的嘗試。當選擇 Conflict.ContinueOnConflict 時,系統會嘗試運行對資料庫的所有更新。
成員名稱 | 說明 |
FailOnFirstConflict | 指定當檢測到第一個併發衝突錯誤時,應立即停止對更新資料庫的嘗試。 |
ContinueOnConflict | 指定應嘗試對資料庫的所有更新,並且應在該過程結束時累積和返回併發衝突。 |
ConfilctMode成員圖
ResolveAll 能使用指定策略解決集合中的所有衝突,當選擇 RefreshMode.KeepChanges 時,系統會強制 Refresh 方法保持當前上下文的數據值。當選擇RefreshMode.OverwriteCurrentValues, 系統會用資料庫的值覆蓋當前上下文中的數據值。當選擇 RefreshMode.KeepCurrentValues, 系統會把當前上下文的更新值與資料庫中的值進行合併處理。
成員名稱 | 說明 |
OverwriteCurrentValues | 強制 Refresh 方法使用資料庫中的值重寫所有當前值。 |
KeepChanges | 強制 Refresh 方法保留已更改的當前值,但將其他值更新為資料庫值。 |
KeepCurrentValues | 強制 Refresh 方法使用從資料庫檢索的值替換原始值。 不會修改當前值。 |
RefreshMode 成員圖
當 Person 表格的多個列的 UpdateCheck 屬性都為預設值 Always 時,多個客戶同時更新此數據表,最後使用 DataContext.SubmitChanges(ConflictMode.ContinuOnConflict) 同時提交數據時,系統就會釋放出 ChangeConflictException 異常。系統可以以捕獲此併發異常後,再決定採取 KeepChanges、KeepCurrentValues、OverwriteCurrentValues 等方式處理數據。
1 public class PersonDAL 2 { 3 delegate void MyDelegate(Person person); 4 5 static void Main(string[] args) 6 { 7 //在更新數據前顯示對象信息 8 PersonDAL personDAL = new PersonDAL(); 9 var beforeObj = personDAL.GetPerson(52); 10 personDAL.DisplayProperty("Before", beforeObj); 11 12 //更新Person的FirstName、SecondName屬性 13 Person person1 = new Person(); 14 person1.Id = 52; 15 person1.FirstName = "Mike"; 16 person1.SecondName = "Wang"; 17 person1.Age = 32; 18 person1.Address = "Tianhe"; 19 person1.Telephone = "13660123456"; 20 person1.EMail = "[email protected]"; 21 22 //更新Person的FirstName、SecondName屬性 23 Person person2 = new Person(); 24 person2.Id = 52; 25 person2.FirstName = "Rose"; 26 person2.SecondName = "Chen"; 27 person2.Age = 32; 28 person2.Address = "Tianhe"; 29 person2.Telephone = "13660123456"; 30 person2.EMail = "[email protected]"; 31 32 //使用非同步方式更新數據 33 MyDelegate myDelegate = new MyDelegate(personDAL.Update); 34 myDelegate.BeginInvoke(person1, null, null); 35 myDelegate.BeginInvoke(person2, null, null); 36 37 //顯示更新後的對象信息 38 Thread.Sleep(1000); 39 var afterObj = personDAL.GetPerson(52); 40 personDAL.DisplayProperty("After", afterObj); 41 Console.ReadKey(); 42 } 43 44 public void Update(Person person) 45 { 46 using (BusinessDataContext context = new BusinessDataContext()) 47 { 48 try 49 { 50 var person1 = context.Person.Where(x => x.Id == person.Id).First(); 51 if (person1 != null) 52 { 53 person1.Address = person.Address; 54 person1.Age = person.Age; 55 person1.EMail = person.EMail; 56 person1.FirstName = person.FirstName; 57 person1.SecondName = person.SecondName; 58 person1.Telephone = person.Telephone; 59 } 60 //虛擬操作,保證多個值同時提交 61 Thread.Sleep(100); 62 context.SubmitChanges(ConflictMode.ContinueOnConflict); 63 DisplayProperty("SubmitChanges Success",person); 64 } 65 catch (ChangeConflictException ex) 66 { 67 //保持最新輸入的上下文數據 68 DisplayProperty("ChangeConflictException", person); 69 context.ChangeConflicts.ResolveAll(RefreshMode.KeepChanges); 70 context.SubmitChanges(); 71 } 72 } 73 } 74 75 public Person GetPerson(int id) 76 { 77 using (BusinessDataContext context = new BusinessDataContext()) 78 { 79 var person = context.Person.Where(x => x.Id == id); 80 return person.First(); 81 } 82 } 83 84 //顯示對象相關屬性 85 public void DisplayProperty(string message, Person person) 86 { 87 String data = string.Format("{0}\n Person Message:\n Id:{1} FirstName:{2} " + 88 "SecondName:{3} Age:{4}\n Address:{5} Telephone:{6} EMail:{7}\n", 89 message, person.Id, person.FirstName, person.SecondName, person.Age, 90 person.Address, person.Telephone, person.EMail); 91 Console.WriteLine(data); 92 } 93 }
例子當中使用 RefreshMode.KeepChanges 的方式處理併發,系統會保存最新輸入的數據。觀察測試結果,當系統發生第一次更新時,數據成功保存到資料庫當中。在 DataContext 未釋放前,再次輸入數據,引發了ChangeConflictException異常。在捕獲此併發異常後,系統以 RefreshMode.KeepChanges 方式進行處理,最後新輸入的數據成功保存到資料庫當中。
回到目錄
六、結合事務處理併發衝突
Entity Framework 中已經有比較完善的機制處理併發,但使用樂觀性併發處理數據,一旦多個客戶端同時更新同一張表格的同一個對象時,將會激發 OptimisticConcurrencyException 異常,系統必須預先定製好處理方案對此併發異常進行處理。結合事務使用樂觀性併發共同處理數據,是一個比較高效的數據管理方式。事務能對數據的更新進行檢測,一旦發現異常,便會實現回滾。本文會使用常用的隱式事務 TransactionScope 作為例子進行介紹,對事務的詳細介紹,可以參考 “C#綜合揭秘——細說事務”。
使用樂觀性併發,在發生併發異常時保留最初輸入值,並利用事務,對引起 OptimisticConcurrencyException 異常的修改進行回滾,最後把引起異常的數據與資料庫中的已有數據以頁面或者視窗的形式顯示給客戶進行對比,這是最為常用的數據處理方式之一。這樣既可以保持系統高效運行,也可以避免系統數據出現邏輯性的錯誤。
下麵舉個例子,對此方法進行說明。首先把 Person 對象的 FirstName、SecondName 屬性的 ConcurencyMode 設置為 Fixed 後,系統會同時更新 Id 等於 52 的 Person 對象這兩個屬性,更新的操作會以 TransactionScope 進行監控。在同時更新屬性時,系統會釋放出OptimisticConcurrencyException 異常,系統捕獲此異常後會顯示出資料庫中的數據與客戶輸入的數據讓客戶進行對比,並同時實現回滾。
1 public class PersonDAL 2 { 3 delegate void MyDelegate(Person person); 4 5 public static void Main(string[] args) 6 { 7 //在更新數據前顯示對象信息 8 PersonDAL personDAL = new PersonDAL(); 9 var beforeObj = personDAL.GetPerson(52); 10 personDAL.DisplayProperty("Before\n Person", beforeObj); 11 12 //更新Person的FirstName、SecondName屬性 13 Person person1 = new Person(); 14 person1.Id = 52; 15 person1.FirstName = "Mike"; 16 person1.SecondName = "Wang"; 17 person1.Age = 32; 18 person1.Address = "Tianhe"; 19 person1.Telephone = "13660123456"; 20 person1.EMail = "[email protected]"; 21 22 //更新Person的FirstName、SecondName屬性 23 Person person2 = new Person(); 24 person2.Id = 52; 25 person2.FirstName = "Rose"; 26 person2.SecondName = "Chen"; 27 person2.Age = 32; 28 person2.Address = "Tianhe"; 29 person2.Telephone = "13660123456"; 30 person2.EMail = "[email protected]"; 31 32 //使用非同步方式更新數據 33 MyDelegate myDelegate = new MyDelegate(personDAL.Update); 34 myDelegate.BeginInvoke(person1, null, null); 35 myDelegate.BeginInvoke(person2, null, null); 36 37 //顯示更新後資料庫對象信息 38 Thread.Sleep(1000); 39 var afterObj = personDAL.GetPerson(52); 40 personDAL.DisplayProperty("After\n Person", afterObj); 41 42 Console.ReadKey(); 43 } 44 45 public Person GetPerson(int id) 46 { 47 using (BusinessEntities context = new BusinessEntities()) 48 { 49 IQueryable<Person> list=context.Person.Where(x => x.Id == id); 50 return list.First(); 51 } 52 } 53 54 //更新對象 55 public void Update(Person person) 56 { 57 using (BusinessEntities context = new BusinessEntities()) 58 { 59 var obj = context.Person.Where(x => x.Id == person.Id).First(); 60 if (obj != null) 61 { 62 try 63 { 64 using (TransactionScope scope = new TransactionScope()) 65 { 66 context.ApplyCurrentValues("Person", person); 67 //虛擬操作,保證數據被同步載入 68 Thread.Sleep(100); 69 int n = context.SaveChanges(); 70 scope.Complete(); 71 } 72 } 73 catch (System.Data.OptimisticConcurrencyException ex) 74 { 75 //可以根據需要,以錯誤頁面或者提示視窗等方式處理 76 //顯示發生OptimisticConcurrencyException異常所輸入的數據屬性 77 DisplayProperty("OptimisticConcurrencyException\n Client Value:", person); 78 DisplayProperty(" Database Value:", GetPerson(person.Id)); 79 } 80 catch(Exception ex) 81 {......} 82 } 83 } 84 } 85 86 //顯示對象相關屬性 87 public void DisplayProperty(string message, Person person) 88 { 89 String data = string.Format("{0}\n Id:{1} FirstName:{2} " + 90 "SecondName:{3} Age:{4}\n Address:{5} Telephone:{6} EMail:{7}\n", 91 message, person.Id, person.FirstName, person.SecondName, person.Age, 92 person.Address, person.Telephone, person.EMail); 93 Console.WriteLine(data); 94 } 95 }
觀察測試結果,當系統發生併發異常時,資料庫只會保留首次輸入的更新值。值得註意的是:使用此數據處理方式與悲觀併發方式最大區別在於,傳統的悲觀併發處理方式不允許同一時刻有多個客戶端處理同一個數據表中的相同對象,但因為客觀因數的影響,系統難以仔細辨認客戶同時進行修改的是否同一個數據項,所以基本的做法是使用鎖把更新對象進行鎖定。但如此一來,無論客戶同時更新的是否同一個數據項,操作都將會被延時或禁止。換句話說,無論需要更新的是相同對象還是不同對象,客戶端都不能同時更新同一個數據表。若使用此樂觀併發方式,系統允許多個客戶端同時處理同一個數據表。如果所處理的是數據表中的不同對象,操作可以順利地進行而不會相互影響。如果所處理的是數據表中的相同對象,操作將會保存第一次輸入的對象值。
回到目錄
總結
併發話題與線程、進程等其他話題有所不同,它並沒有複雜的類和方法。處理併發來來去去都是簡單的幾行代碼,它所重視的是併發異常發生後所帶來的後果與處理方式。與中國傳統的太極相似,併發只重其意,不重其招,只要深入地瞭解其過程,考慮其可能帶來的問題後,你便可以對其收發自如。
對併發的處理應該針對特定的問題,分別對待。到最後你可能發現,原來微軟早已為你定製好處理的方式,可能 “回到原點” 什麼都不做就是最好的處理方式。