引言
在軟件開發過程中,並發控制是確保及時糾正由並發操作導致的錯誤的一種機制。從 ADO.NET 到 LINQ to SQL 再到如今的 ADO.NET Entity Framework,.NET 都為並發控制提供好良好的支持方案。
並發處理方式一般分為樂觀必並發與悲觀必並發兩種,本文將為大家介紹 Entity Framework 、 LINQ to SQL 中的並發處理方式。在本文最后,將提供一個了可參考的方案,結合事務 Transaction 處理復雜性對象的並發。
目錄
一、並發處理的定義
在軟件開發過程中,當多個用戶同時修改一條數據記錄時,系統需要預先制定對並發的處理模式。並發處理模式主要分為兩種:
第一種模式稱為悲觀式並發,即當一個用戶已經在修改某條記錄時,系統將拒絕其他用戶同時修改此記錄。
第二種模式稱為樂觀式並發,即系統允許多個用戶同時修改同一條記錄,系統會預先定義由數據並發所引起的並發異常處理模式,去處理修改后可能發生的沖突。常用的樂觀性並發處理方法有以下幾種:
- 保留最后修改的值。
- 保留最初修改的值。
- 合並多次修改的值。
相對於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 private static object o=new object(); 2 3 public int Update(Person person) 4 { 5 int n = -1; 6 try 7 { 8 lock (o) 9 { 10 using (BusinessEntities context = new BusinessEntities()) 11 { 12 var obj = context.Person.Where(x => x.Id == person.Id).First(); 13 if (obj != null) 14 context.ApplyCurrentValues("Person", person); 15 n = context.SaveChanges(); 16 } 17 } 18 } 19 catch (Exception ex) 20 { ...... } 21 return n; 22 }
使用悲觀並發雖然能有效避免數據發生邏輯性的錯誤,但使用 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 同時更新數據
在系統輸入下面代碼,獲取數據庫中的Person:Id 為24,FirstName為Leslie, SecondName為Lee。然后使用異步方法分兩次調用Update方法,同時更新Person對象的相關屬性,第一次更新把對象的 FirstName 屬性改Rose,第二次更新把對象的 SecondName改為Wang,Age改為32。在使用SaveChanges保存更新時,第一個方法已經把ObjectContext1中的FirstName修改為Rose,但在第二個方法中的ObjectContext2中的FirstName依然是Leslie,此時由於三個屬性的ConcurencyMode都為默認值None,系統會忽略當中的沖突,而接受所有的更新修改。
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 = "Leslie@163.com"; 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 = "Leslie@163.com"; 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 = "Leslie@163.com"; 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 顯示更新后的數據屬性。在第二次更新數據時,由調用ObjectContext.SaveChanges時,數據庫中的數據已經被修改,與當前上下文ObjectContext 的數據存在沖突,系統將激發 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 = "Leslie@163.com"; 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 = "Leslie@163.com"; 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 = "Leslie@163.com"; 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 = "Leslie@163.com"; 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 = "Leslie@163.com"; 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 = "Leslie@163.com"; 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#綜合揭秘——細說事務”。其實在CodePlex網站可以看到,微軟在ObjectContext.Savechanges方法的實現中,已經使用了事務用於保證數據輸入的正確性。但對於一部分多表操作的方法中,依然需要使用事務在多表更新時保證一致性。
在實際開發過程當中,最常遇見的是在訂單更新的過程,系統會把客戶的個人信息,收貨地址,聯系方式,訂單總體表,訂單明細表等多表格的數據同時提交。此時,可以選擇不同的方法進行處理。
在Order與OrderItem這種主從關系表中,可以使用把次表格原有的有關數據全部刪除,然后根據新輸入的對象重新加載的方法進行處理。此處理方法的好處在於,避免新輸入的數據與次表格的原有數據存在沖突,但為此一旦發生並發就有可能引起多次加載的危險。
對於 Person,Order 這樣不同的實體,一般會把操作放置於不同的操作對象中,以不同的ObjectContext進行處理。為了保證操作具有一致性,可使用事務把多個表格的操作合並在一起。此時,即使系統發生異常,多個表數據也能實現同步回滾,避免引起邏輯上的錯誤。
但使用事務必須注意,一旦發生並發,容易引發事務鎖異常: "事務(進程 ID 53)與另一個進程被死鎖在 鎖 資源上,並且已被選作死鎖犧牲品 ...... " 。這是因為多個事務被同時觸發,在實現更新的過程中事務都對部分數據實現鎖定而引起。所以使用事務時需要引入鎖來避免此情況出來。
1 public class OrderDAL 2 { 3 public void UpdateOrder(Order order) 4 { 5 using (BusinessEntities context = new BusinessEntities()) 6 { 7 Order objResult = context.Order.Include("OrderItem") 8 .Where(x => x.Id == order.Id).First(); 9 10 if (objResult != null) 11 { 12 try 13 { 14 //把原有相關的OrderItem對象全部刪除 15 foreach (var item in objResult.OrderItem.ToList()) 16 context.OrderItem.DeleteObject(item); 17 //更新 Order對象的屬性,加入對應新的OrderItem對象 18 context.ApplyCurrentValues("BusinessEntities.Order", order); 19 foreach (var item in order.OrderItem.ToList()) 20 { 21 //先把OrderItem的導航屬性Order變為空值 22 //否則系統會顯示重復插入的異常 23 item.Order = null; 24 context.OrderItem.AddObject(item); 25 } 26 context.SaveChanges(); 27 } 28 catch (System.Data.OptimisticConcurrencyException ex) 29 {......} 30 catch (Exception ex) 31 {......} 32 } 33 } 34 } 35 } 36 37 public class PersonDAL 38 { 39 //更新Person對象 40 public void Update(Person person) 41 { 42 using (BusinessEntities context = new BusinessEntities()) 43 { 44 var obj = context.Person.Where(x => x.Id == person.Id).First(); 45 if (obj != null) 46 context.ApplyCurrentValues("Person", person); 47 context.SaveChanges(); 48 } 49 } 50 } 51 52 public class UpdateOrder 53 { 54 private OrderDAL orderDAL = new OrderDAL(); 55 private PersonDAL personDAL = new PersonDAL(); 56 57 public void DoWork(Person person, Order order) 58 { 59 lock(this) 60 { 61 //操作時將同時更新Order、OrderItem、Person三個表格 62 //為了避免其中一個操作出現錯誤而引起數據邏輯性錯誤 63 //應使用事務作為保護,在出現錯誤時實現同步回滾 64 using (TransactionScope scope = new TransactionScope()) 65 { 66 orderDAL.UpdateOrder(order); 67 personDAL.Update(person); 68 scope.Complete(); 69 } 70 } 71 } 72 }
在復雜的多表數據處理過程中,推薦使用事務結合鎖來進行處理。
值得注意的是:使用樂觀並發與悲觀並發方式最大區別在於,傳統的悲觀並發處理方式不允許同一時刻有多個客戶端處理同一個數據表中的相同對象,但因為客觀因數的影響,系統難以仔細辨認客戶同時進行修改的是否同一個數據項,所以基本的做法是使用鎖把更新對象進行鎖定。但如此一來,無論客戶同時更新的是否同一個數據項,操作都將會被延時或禁止。換句話說,無論需要更新的是相同對象還是不同對象,客戶端都不能同時更新同一個數據表。在處理復雜的多表數據過程中,可以考慮使用此處理方式。
若使用樂觀並發方式,系統允許多個客戶端同時處理同一個數據表。如果所處理的是數據表中的不同對象,操作可以順利地進行而不會相互影響。如果所處理的是數據表中的相同對象,可以保守處理,保存第一次輸入的對象值,然后把引起異常的數據顯示讓客戶進行對比。
總結
並發話題與線程、進程等其他話題有所不同,它並沒有復雜的類和方法。處理並發來來去去都是簡單的幾行代碼,它所重視的是並發異常發生后所帶來的后果與處理方式。與中國傳統的太極相似,並發只重其意,不重其招,只要深入地了解其過程,考慮其可能帶來的問題后,你便可以對其收發自如。
對並發的處理應該針對特定的問題,分別對待。到最后你可能發現,原來微軟早已為你定制好處理的方式,可能 “回到原點” 什么都不做就是最好的處理方式。
希望此文章對讓大家有所幫助,歡迎各位提供寶貴意見,指出文中存在錯誤或漏洞。
對 .NET 開發有興趣的朋友歡迎加入QQ群:230564952 共同探討 !
C#綜合揭秘
通過修改注冊表建立Windows自定義協議
Entity Framework 並發處理詳解
細說進程、應用程序域與上下文
細說多線程(上)
細說多線程(下)
細說事務
深入分析委托與事件
作者:風塵浪子
http://www.cnblogs.com/leslies2/archive/2012/07/30/2608784.html
原創作品,轉載時請注明作者及出處