淺析Entity Framework Core中的並發處理


前言

Entity Framework Core 2.0更新也已經有一段時間了,園子里也有不少的文章..

本文主要是淺析一下Entity Framework Core的並發處理方式.

 

 

1.常見的並發處理策略

要了解如何處理並發,就要知道並發的一般處理策略

悲觀並發策略

悲觀並發策略,正如其名,它指的是對數據被外界(包括本系統當前的其他事務,以及來自外部系統的事務處理)修改持保守悲觀的態度,因此,在整個數據處理過程中,將數據處於鎖定狀態。悲觀並發策略大多數情況下依靠數據庫的鎖機制實現,以保證操作最大程度的獨占性。但隨之而來的就是數據庫性能的巨大開銷,特別是對長事務而言,這樣的開銷在大量的並發情況下往往無法承受。


樂觀並發策略

樂觀並發策略,一般是基於數據版本 Version記錄機制實現。何謂數據版本?即為數據增加一個版本標識,在基於數據庫表的版本解決方案中,一般是通過為數據庫表增加一個 “version” 字段來實現.讀取出數據時,將此版本號一同讀出,之后更新時,對此版本號加一。此時,將提交數據的版本數據與數據庫表對應記錄的當前版本信息進行比對,如果提交的數據版本號大於數據庫表當前版本號,則予以更新,否則認為是過期數據。需要注意的是,樂觀並發策略機制往往基於系統中的數據存儲邏輯,因此也具備一定的局限性.

本篇就是講解,如何在我們的Entity Framework Core中來使用和自定義我們的並發策略

 

 

2.Entity Framework Core並發令牌

要使用Entity Framework Core中的並發策略,就需要使用我們的並發令牌(ConcurrencyCheck)

在Entity Framework Core中,並發的默認處理方式是無視並發沖突的,任何修改語句在條件符合的情況下,都可以修改成功.

在高並發的情況下這種處理方式,肯定會給我們的數據庫帶來很多臟數據,所以,Entity Framework Core提供了並發令牌(ConcurrencyCheck)這個特性.

如果一個屬性被配置為並發令牌,則EF將在保存這條記錄時,會檢查沒有其他用戶修改過數據庫中的這個屬性的值。EF使用了樂觀並發策略,這意味着它將假定值沒有改變,並嘗試保存數據,但如果發現值已更改,則拋出異常。

舉個例子,我們有一個用戶類(User),我們配置 User中的 Name為並發令牌。這意味着,如果一個用戶試圖保存一個有些變化的 User,但另一個用戶已經改變了 Name那么將拋出一個異常。這在應用中一般是可取的,以便我們的應用程序可以提示用戶,在保存他們的改變之前,以確保此記錄仍然代表同一個姓名的人。

2.1並發令牌在EF中工作的原理

當我們配置User中的Name為令牌的時候,EF會將並發令牌包含在Where、Update或delete命令子句中並檢查受影響的行數來實現驗證如果並發令牌仍然匹配,則一行將被更新。如果數據庫中的值已更改,則不會更新任何行。

比如,當我們設置Name為並發令牌,然后通過ID來修改User的PassWord的時候,EF會生成如下的修改語句:

UPDATE [User] SET [PassWord] = @p1
WHERE [ID] = @p0 AND [Name] = @p2;

當然,這時候,Name不匹配了,受影響的行數返回為0.

2.2並發令牌的使用約定

    屬性默認不被配置為並發令牌。

 

2.3並發令牌的使用方式

1.直接使用特性,如下配置UserName為並發令牌:

public partial class UserTable
    {
        public int Id { get; set; }
        [ConcurrencyCheck]
        public string UserName { get; set; }
        public string PassWord { get; set; }
        public int? ClassId { get; set; }
}

 2.使用FluentAPI配置屬性為並發令牌

class MyContext : DbContext
{
    public DbSet<UserTable> People { get; set; }
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<UserTable>()
            .Property(p => p.UserName)
            .IsConcurrencyToken();
    }
}

 

以上2種方式,效果是一樣的.

 

2.4使用時間戳和行級版本號

我們知道,SQL Server給我們提供了時間戳的屬性(當然,幾乎所有的關系數據庫都有這個).下面舉個SQL Server的例子

我們加一個時間戳字段為TimestampV,加上特性Timestamp,實體代碼如下:

  public partial class UserTable
    {
        public int Id { get; set; }

        public string UserName { get; set; }
        public string PassWord { get; set; }
        public int? ClassId { get; set; }

        public ClassTable Class { get; set; }

        [Timestamp]
        public byte[] TimestampV { get; set; }
    }

 

CodeFrist生成的表如下:

自動幫我們生成的Timestamp類型的一個字段.

配置時間戳屬性的方式也有2種,上面已經說了一種..特性的..

同樣我們也可以使用Fluent API配置屬性為時間戳,代碼如下:

class MyContext : DbContext
{
    public DbSet<UserTable> Blogs { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<UserTable>()
            .Property(p => p.TimestampV)
            .ValueGeneratedOnAddOrUpdate()
            .IsConcurrencyToken();
    }
}

 

 

3.如何根據需求自定義處理並發沖突

上面,我們已經配置好了需要並發處理的表,也配置好了相關的特性,下面我們就來講講如何使用它.

使用之前,我們先來了解一下,並發過程中所產生的3個值,也是我們需要處理的3個值

       1.當前值是應用程序嘗試寫入數據庫的值。

       2.原始值是在進行任何編輯之前最初從數據庫檢索的值。

       3.數據庫值是當前存儲在數據庫中的值。

當我們配置好上面的並發令牌時,在EF執行SaveChanges()操作並產生並發的時候,我們會得到DbUpdateConcurrencyException的異常信息,(注意:在不配置並發令牌時,這個異常一般不會觸發)

前面,我們已經講過樂觀並發策略是一種性能較高,也比較實用的處理方式,所以我們就通過時間戳來處理這個並發的問題.

示例測試代碼如下:

 public void Test()
 {
            //重新創建數據庫,並新增一條數據
            using (var context = new School_TestContext())
            {
                context.Database.EnsureDeleted();
                context.Database.EnsureCreated();

                context.UserTable.Add(new UserTable { UserName = "John", PassWord = "Doe" });
                context.SaveChanges();
            }

            using (var context = new School_TestContext())
            {
                // 修改id為1的用戶名稱
                var person = context.UserTable.Single(p => p.Id == 1);
                person.UserName = "555-555-5555";

                // 直接通過訪問數據庫來修改同一條數據 (這里是為了模擬並發)
                context.Database.ExecuteSqlCommand("UPDATE dbo.UserTable SET UserName = 'Jane' WHERE ID = 1");

                try
                {
                    //嘗試保存修改
                    int a = context.SaveChanges();
                }
                //獲取並發異常
                catch (DbUpdateConcurrencyException ex)
                {
                    foreach (var entry in ex.Entries)
                    {
                        if (entry.Entity is UserTable)
                        {
                            var databaseEntity = context.UserTable.AsNoTracking().Single(p => p.Id == ((UserTable)entry.Entity).Id);
                            var databaseEntry = context.Entry(databaseEntity);

                            //當前上下文時間戳
                            var date = ConvertToTimeSpanString(entry.Property("TimestampV").CurrentValue);
                            var dateint = Int32.Parse(date, System.Globalization.NumberStyles.HexNumber);

                            //數據庫時間戳
                            var datebase = ConvertToTimeSpanString(databaseEntry.Property("TimestampV").CurrentValue);
                            var dateint2 = Int32.Parse(datebase, System.Globalization.NumberStyles.HexNumber);
                            //如果當前上下文時間戳與數據庫相同,或者更加新,則使用當前
                            if (dateint >= dateint2)
                            {
                                foreach (var property in entry.Metadata.GetProperties())
                                {
                                    //當前值
                                    var proposedValue = entry.Property(property.Name).CurrentValue;

                                    //原始值
                                    var originalValue = entry.Property(property.Name).OriginalValue;

                                    //數據庫值
                                    var databaseValue = databaseEntry.Property(property.Name).CurrentValue;

                                    //更新當前值
                                    entry.Property(property.Name).CurrentValue = proposedValue;

                                    //更新原始值來保證修改成功
                                    entry.Property(property.Name).OriginalValue = databaseEntry.Property(property.Name).CurrentValue;
                                    // 嘗試重新保存數據
                                    int aa = context.SaveChanges();
                                }
                            }
                        }
                        else
                        {
                            throw new NotSupportedException("無法處理並發," + entry.Metadata.Name);
                        }
                    }


                }
            }

        }

 

執行這段代碼,會發現,符合我們樂觀並發策略的要求.

值為最后修改的UserName,為Jane,如圖:

解釋一下,為何最終結果為Jane.

首先,我們添加了一條UserName為John的數據,我們在上下文中修改它為"555-555-5555",

這時候,產生並發,另一個上下文在這個SaveChang之前,就執行完成了,把值修改為了Jane,所以EF通過並發令牌發現匹配失敗.則會觸發異常.

在異常中,我們將當前上下文的版本號和數據庫現有的版本號進行對比,發現當前上下文的版本號為過期數據,則不更新,並返回失敗.

請仔細看代碼中的注釋.

注意:這里的例子是根據樂觀並發處理策略要進行處理的.你可以根據你的業務,來任意處理當前值,原始值和數據庫值,選擇你需要的值保存.

 

 

 

寫在最后

.net core已經2.0版本了,Asp.net Core也2.0了..EFcore也2.0了..功能已經越來越強大,越來越完善.完全可以投入生產了.園子里對這些新技術也很關注,真的...我感覺很棒..從未如此的棒!!!!


免責聲明!

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



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