EntityFramework 6 (EF6 DBcontext) 並發處理實戰


 

學習:C#綜合揭秘——Entity Framework 並發處理詳解 帖子筆記 ,該帖子使用的是objectContext ,

一、並發相關概念

並發的類型:

第一種模式稱為悲觀式並發,即當一個用戶已經在修改某條記錄時,系統將拒絕其他用戶同時修改此記錄。
第二種模式稱為樂觀式並發,即系統允許多個用戶同時修改同一條記錄,系統會預先定義由數據並發所引起的並發異常處理模式,去處理修改后可能發生的沖突。常用的樂觀性並發處理方法有以下幾種:

    1、保留最后修改的值。
    2、保留最初修改的值。
    3、合並多次修改的值。

二、模型屬性的並發處理選項

如下圖模型設計器中TimeStamp字段為啟用並發

image

<EntityType Name="UserAccout">
          <Key>
            <PropertyRef Name="Id" />
          </Key>
          <Property Name="Id" Type="Int32" Nullable="false" annotation:StoreGeneratedPattern="Identity" />
          <Property Name="FirstName" Type="String" Nullable="false" />
          <Property Name="LastName" Type="String" Nullable="false" />
          <Property Name="AuditFileds" Type="OrderDB.AuditFields" Nullable="false" />
          <Property Name="Timestamp" Type="DateTime" Nullable="false" ConcurrencyMode="Fixed" annotation:StoreGeneratedPattern="Computed" />
        </EntityType>

並發模式:ConcurencyMode 有兩個成員:

None : 在寫入時從不驗證此屬性。 這是默認的並發模式。

Fixed: 在寫入時始終驗證此屬性。

當模型屬性為默認值 None 時,系統不會對此模型屬性進行檢測,當同一個時間對此屬性進行修改時,系統會以數據合並方式處理輸入的屬性值。

當模型屬性為Fixed 時,系統會對此模型屬性進行檢測,當同一個時間對屬性進行修改時,系統就會激發OptimisticConcurrencyException 異常。

 

三、悲觀並發

 

四、樂觀並發

為了解決悲觀並發所帶來的問題,ADO.NET Entity Framework 提供了更為高效的樂觀並發處理方式。相對於LINT to SQL , ADO.NET Entity Framework 簡化了樂觀並發的處理方式,它可以靈活使用合並數據、保留初次輸入數據、保留最新輸入數據(3種方式)等方式處理並發沖突。

4.1 以合並方式處理並發數據

總結:當模型屬性的 ConcurencyMode 為默認值 None ,一旦同一個對象屬性同時被修改,系統將以合並數據的方式處理並發沖突,這也是 Entity Framework 處理並發沖突的默認方式。

合並處理方式如下:

(1)當同一時間針對同一個對象屬性作出修改,系統將保存最新輸入的屬性值

(2)當同一時間對同一對象的不同屬性作出修改,系統將保存已被修改的屬性值。下面用兩個例子作出說明:

image

運行結果:

image

#region (4.1)測試不設置任何並發測試時,當產生並發EF的處理方法
        delegate void MyDelegate(Address addressValue);
        public  StringBuilder sb = new StringBuilder();
        public Address GetAddress(int id)
        {
            using (OrderDBContainer context = new OrderDBContainer())
            {
                IQueryable<Address> list = context.AddressSet.Where(x => x.Id == id);
                return list.First();
            }
        }
        /// <summary>
        /// 修改方法
        /// </summary>
        /// <param name="addressValue"></param>
        public void UpdateAddress(Address addressValue)
        {
            using (OrderDBContainer context = new OrderDBContainer())
            {
                //顯示輸入新數據的信息
                Display("Current", addressValue);
                var obj = context.AddressSet.Where(x => x.Id == addressValue.Id).First();
                if (obj != null)
                    context.Entry(obj).CurrentValues.SetValues(addressValue);
                //虛擬操作,保證數據能同時加入到上下文當中
                Thread.Sleep(100);
                context.SaveChanges();
            }
        }        
        /// <summary>
        /// 顯示實體當前屬性
        /// </summary>
        /// <param name="message"></param>
        /// <param name="addressValue"></param>
        public void Display(string message, Address addressValue)
        {
            String data = string.Format("{0}\n  Address Message:\n    Id:{1}  Address1:{2}  " +
                "address2:{3} \r\n ",
                message, addressValue.Id, addressValue.Address1, addressValue.Address2 );
            sb.AppendLine(data);
        }     
        
        /// <summary>
        /// (1)測試使用EF默認的機制,當配置並發控制時,系統是使用的合並的方式
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void button3_Click(object sender, EventArgs e)
        {
            //在更新數據前顯示對象信息
            var beforeObj = GetAddress(1);
            Display("Before", beforeObj);

            //更新Person的SecondName,Age兩個屬性
            Address _address1 = new Address();
            _address1.Id = 1;
            _address1.Address1 = "古溪";
            _address1.Address2 = beforeObj.Address2;
            _address1.AuditFields.InsertDate = beforeObj.AuditFields.InsertDate;
            _address1.AuditFields.UpdateDate = beforeObj.AuditFields.UpdateDate;
            _address1.City = beforeObj.City;
            _address1.Zip = beforeObj.Zip;
            _address1.State = beforeObj.State;

            //更新Person的FirstName屬性
            Address _address2 = new Address();
            _address2.Id = 1;
            _address2.Address1 = beforeObj.Address1;
            _address2.Address2 = "江蘇";
            _address2.AuditFields.InsertDate = beforeObj.AuditFields.InsertDate;
            _address2.AuditFields.UpdateDate = beforeObj.AuditFields.UpdateDate;
            _address2.City = beforeObj.City;
            _address2.Zip = beforeObj.Zip;
            _address2.State = beforeObj.State;

            //使用異步方式同時更新數據
            MyDelegate myDelegate = new MyDelegate(UpdateAddress);
            myDelegate.BeginInvoke(_address1, null, null);
            myDelegate.BeginInvoke(_address2, null, null);

            Thread.Sleep(1000);
            //在更新數據后顯示對象信息
            var afterObj = GetAddress(1);
            Display("After", afterObj);
            this.textBox1.Text = sb.ToString();
        }

        /// <summary>
        /// 先插入幾條數據等着測試
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void BtnSaveAddress_Click(object sender, EventArgs e)
        {
            using (OrderDBContainer db = new OrderDBContainer())
            {
                Address address = new Address();
                address.Address1 = "古溪鎮";
                address.Address2 = "安鎮";
                address.State = "2";
                address.City = "無錫";
                address.AuditFields.InsertDate = DateTime.Now;
                address.AuditFields.UpdateDate = DateTime.Now;
                address.Zip = "21415";
                db.AddressSet.Add(address);
                db.SaveChanges();
            }
        }

        /// <summary>
        /// 還原成初始值,准備再次測試
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void button5_Click(object sender, EventArgs e)
        {
            using (OrderDBContainer db = new OrderDBContainer())
            {
                Address _address = db.AddressSet.Where(x => x.Id == 1).First();
                _address.Address1 = "aaa";
                _address.Address2 = "bbb";
                db.SaveChanges();
            }
        }
        #endregion

備注:實踐過程中遇到的問題

在多線程中EF修改事件的解決方案,使用attach不可以:

image

使用Entry也報錯

image

最終參考如下帖子

image

/// <summary>
        /// 修改方法
        /// </summary>
        /// <param name="addressValue"></param>
        public void UpdateAddress(Address addressValue)
        {
            using (OrderDBContainer context = new OrderDBContainer())
            {
                //顯示輸入新數據的信息
                Display("Current", addressValue);
                var obj = context.AddressSet.Where(x => x.Id == addressValue.Id).First();
                if (obj != null)
                    context.Entry(obj).CurrentValues.SetValues(addressValue);
                //虛擬操作,保證數據能同時加入到上下文當中
                Thread.Sleep(100);
                context.SaveChanges();
            }
        }

引用:“以合並數據的方式處理並發沖突固然方便快節,但在業務邏輯較為復雜的系統下並不適合使用此處理方式。比如在常見的Order、OrderItem的表格中,OrderItem 的單價,數量會直接影響Order的總體價格,這樣使用合並數據的方式處理並發,有可能引起邏輯性的錯誤。此時,應該考慮以其他方式處理並發沖突。”。

其他什么方式呢?【待補充】

 

4.1 刪除與更新操作同時運行(非框架自動處理能力,開發自行修改狀態手動增加的

Entity Framework 能以完善的機制靈活處理同時更新同一對象的操作,但一旦刪除操作與更新操作同時運行時,就可能存在邏輯性的異常。

例如:兩個客戶端同時加載了同一個對象,第一個客戶端更新了數據后,把數據再次提交。但在提交前,第二個客戶端已經把數據庫中的已有數據刪除。

此時,上下文中的對象處於不同的狀態下,將會引發 OptimisticConcurrencyException 異常(ObjectContext 與DBContext兩種方式下,異常不一樣,具體要根據測試結果自己判斷)。
遇到此異常時,可以用 try(OptimisticConcurrencyException){...} catch {...} 方式捕獲異常,然后更改對象的State 屬性。把EntityState 更改為 Added ,被刪除的數據便會被再次加載。若把 EntityState 更改為 Detached 時,數據便會被順利刪除。下面把對象的 EntityState 屬性更改為 Added 作為例子。

image

代碼如下:處理結果前后ID變化了(或許這就是有些架構師使用手動創建的GUID的方式,而不使用自增的原因之一吧,因為數據刪除后再創建就回不到之前的ID了,不是太靈活,使用GUID再結合數據版本(dataVison)字段,timeStamp基本上控制數據的並發已經足夠啊。

//更新對象
        public int UpdateWithConcurrent(int num, Address addressValue)
        {
            int returnValue = -1;
            using (OrderDBContainer context = new OrderDBContainer())
            {
                var obj = context.AddressSet.Where(x => x.Id == addressValue.Id).First();
                //顯示對象所處狀態
                DisplayState("Before Update", obj);
                try
                {
                    if (obj != null)
                        context.Entry(obj).CurrentValues.SetValues(addressValue);
                    //虛擬操作,保證數據已經在數據庫中被異步刪除
                    Thread.Sleep(300);
                    context.SaveChanges();
                    returnValue = obj.Id;
                }
                catch (Exception)
                {
                    //針對異常要做相應的判斷,因為我只測試了刪除的情況,就寫死直接修改成Added 了
                    //正確的是要區分到底是修改還是刪除  OptimisticConcurrencyException ex
                    //把對象的狀態更改為 Added
                    context.Entry(obj).State = System.Data.Entity.EntityState.Added;
                    context.SaveChanges();
                    returnValue = obj.Id;
                }
            }
            return returnValue;
        }

並發時的異常類型:

image

ID發生了變化

image

 

4.3 當發生數據並發時,保留最終(最新:最后一次)輸入的數據

要驗證輸入對象的屬性,必須先把該屬性的 ConcurencyMode 設置為 Fixed,這樣系統就會實時檢測對象屬性的輸入值 。
當該屬性被同時更新,系統便會激發 OptimisticConcurrencyException 異常。捕獲該異常后,可以使用 ObjectContext.Refresh (RefreshMode,object) 刷新上下文中該對象的狀態,當 RefreshMode 為 ClientWins 時,系統將會保持上下文中的現在有數據,即保留最新輸入的對象值。此時再使用ObjectContext.SaveChanges, 系統就會把最新輸入的對象值加入數據庫當中。

在下面的例子當,系統啟動前先把 Person 的 FirstName、SecondName 兩個屬性的 ConcurencyMode 屬性設置為Fixed,使系統能監視這兩個屬性的更改。所輸入的數據只在FirstName、SecondName 兩個值中作出修改。在數據提交前先以 DisplayProperty 方法顯示數據庫最初的數據屬性,在數據初次更新后再次調用 DisplayProperty 顯示更新后的數據屬性。在第二次更新數據時,由調用ObjectContext.SaveChanges時,數據庫中的數據已經被修改,與當前上下文ObjectContext 的數據存在沖突,系統將激發OptimisticConcurrencyException 異常,此時把引發異常的對象屬性再次顯示出來。對異常進行處理后,顯示數據庫中最終的對象值。

 

 

觀察測試結果,可見當RefreshMode狀態為ClientWins時,系統將會保存上下文當中的對象屬性,使用此方法可以在發生並發異常時保持最新輸入的對象屬性。

 

4.4 當發生數據並發時,保留最早(最初:最早一次)輸入的數據

把對象屬性的 ConcurencyMode 設置為 Fixed 后,同時更新該屬性,將會激發 OptimisticConcurrencyException 異常。此時使用 ObjectContext.Refresh (RefreshMode,object) 刷新上下文中該對象的狀態,當 RefreshMode 為 StoreWins 時,系統就會把數據源中的數據代替上下文中的數據。
因為初次調用 SaveChanges,數據可以成功保存到數據庫。但是在 ObjectContext 並未釋放時,再次使用 SaveChanges 異步更新數據,就會引發OptimisticConcurrencyException 並發異常。當 RefreshMode 為 StoreWins 時,系統就會保留初次輸入的數據屬性。
此例子與上面的例子十分相似,只是把 RefreshMode 改為 StoreWins 而已。在業務邏輯較為復雜的的系統當中,建議使用此方式處理並發異常。在保留最初輸入的數據修改屬性后,把屬性返還給客戶,讓客戶進行對比后再決定下一步的處理方式。

image

image

 

觀察測試結果,可見當 RefreshMode 狀態為 StoreWins 時,系統將會以數據源中的數據代替上下文當中的對象屬性。在業務邏輯較為復雜的的系統當中,建議使用此方式處理並發異常。


鏈接: https://pan.baidu.com/s/1gfu6fZl 密碼: fyb3

練習的源碼,有糾正的錯誤的朋友記得分享


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM