首頁 > 軟體

Entity Framework管理並行

2022-03-05 16:00:15

理解並行

並行管理解決的是允許多個實體同時更新,實際上這意味著允許多個使用者同時在相同的資料上執行多個資料庫操作。並行是在一個資料庫上管理多個操作的一種方式,同時遵守了資料庫操作的ACID屬性(原子性、一致性、隔離性和永續性)。

想象一下下面幾種可能發生並行的場景:

1、使用者甲和乙都嘗試修改相同的實體。

2、使用者甲和乙都嘗試刪除相同的實體。

3、使用者甲正在嘗試修改一個實體時,使用者乙已經刪除了該實體。

4、使用者甲已經請求讀取一個實體,使用者乙讀完該實體之後更新了它。

這些場景可能會潛在地產生錯誤的資料,試想,成百上千的使用者同時嘗試操作一個相同的實體,這種並行問題將會對系統帶來更大的影響。

在處理與並行相關的問題時,一般有以下兩種方法:

1、樂觀並行:無論何時從資料庫請求資料,資料都會被讀取並儲存到應用記憶體中。資料庫級別沒有放置任何顯示鎖。資料操作會按照資料層接收到的順序執行。

2、悲觀並行:無論何時從資料庫請求資料,資料都會被讀取,然後該資料上就會加鎖,因此沒有人能存取該資料。這會降低並行相關問題的機率,缺點是加鎖是一個昂貴的操作,會降低整個應用程式的效能。

一、理解樂觀並行

前面提到,在樂觀並行中,無論何時從資料庫請求資料,資料都會被讀取並儲存到應用記憶體中。資料庫級別沒有放置任何顯式鎖。因為這種方法沒有新增顯式鎖,所以比悲觀並行更具擴充套件性和靈活性。使用樂觀並行,重點是如果發生了任何衝突,應用程式要親自處理它們。最重要的是:使用樂觀並行控制時,在應用中要有一個衝突解決策略,要讓應用程式的使用者知道他們的修改是否因為衝突的緣故沒有持久化。樂觀並行本質上是允許衝突發生,然後以一種適當的方式解決該衝突。

下面是處理衝突的策略例子。

1、忽略衝突/強制更新

這種策略是讓所有的使用者更改相同的資料集,然後所有的修改都會經過資料庫,這就意味著資料庫會顯示最後一次更新的值。這種策略會導致潛在的資料丟失,因為許多使用者的更改資料都丟失了,只有最後一個使用者的更改是可見的。

2、部分更新

在這種情況中,我們也允許所有的更改,但是不會更新完整的行,只有特定使用者擁有的列更新了。這就意味著,如果兩個使用者更新相同的記錄但卻不同的列,那麼這兩個更新都會成功,而且來自這兩個使用者的更改都是可見的。

3、警告/詢問使用者

當一個使用者嘗試更新一個記錄時,但是該記錄自從他讀取之後已經被其他使用者更改了,這時應用程式就會警告該使用者該資料已經被其他使用者更改了,然後詢問他是否仍然要重寫該資料還是首先檢查已經更新的資料。

4、拒絕更改

當一個使用者嘗試更新一個記錄時,但是該記錄自從他讀取之後已經被其他使用者更改了,此時告訴該使用者不允許更新該資料,因為資料已經被其他使用者更新了。

二、理解悲觀並行

悲觀並行正好和樂觀並行相反,悲觀並行的目標是永遠不讓任何衝突發生。這是通過在使用記錄之前就在記錄上放置顯式鎖實現的。資料庫記錄上可以得到兩種型別的鎖:

唯讀鎖

更新鎖。

當把唯讀鎖放到記錄上時,應用程式只能讀取該記錄。如果應用程式要更新該記錄,它必須要獲取到該記錄上的更新鎖。如果記錄上加了唯讀鎖,那麼該記錄仍然能夠被想要唯讀鎖的請求使用。然而,如果需要更新鎖,該請求必須等到所有的唯讀鎖釋放。同樣,如果記錄上加了更新鎖,那麼其他的請求不能再在這個記錄上加鎖,該請求必須等到已存在的更新鎖釋放才能加鎖。

從前面的描述中,似乎悲觀並行能解決所有跟並行相關的問題,因為我們不必在應用中處理這些問題。然而,事實上並不是這樣的。在使用悲觀並行管理之前,我們需要記住,使用悲觀並行有很多問題和開銷。下面是使用悲觀並行面臨的一些問題:

應用程式必須管理每個操作正在獲取的所有鎖。

加鎖機制的記憶體需求會降低應用效能。

多個請求互相等待需要的鎖,會增加死鎖的可能性。由於這些原因,EF不直接支援悲觀並行。如果想使用悲觀並行的話,我們可以自定義資料庫存取程式碼。此外,當使用悲觀並行時,LINQ to Entities不會正確工作。

三、使用EF實現樂觀並行

使用EF實現樂觀並行有很多方法,接下來我們就看一下這些方法。

1、新建控制檯專案,專案名:EFConcurrencyApp,新聞實體類定義如下:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace EFConcurrencyApp.Model
{
    public class News
    {
        public int Id { get; set; }
        [MaxLength(100)]
        public string Title { get; set; }
        [MaxLength(30)]
        public string Author { get; set; }
        public string Content { get; set; }
        public DateTime CreateTime { get; set; }
        public decimal Amount { get; set; }

    }
}

2、使用資料遷移的方式生成資料庫,並填充種子資料。

namespace EFConcurrencyApp.Migrations
{
    using EFConcurrencyApp.Model;
    using System;
    using System.Data.Entity;
    using System.Data.Entity.Migrations;
    using System.Linq;

    internal sealed class Configuration : DbMigrationsConfiguration<EFConcurrencyApp.EF.EFDbContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = false;
        }

        protected override void Seed(EFConcurrencyApp.EF.EFDbContext context)
        {
            //  This method will be called after migrating to the latest version.

            //  You can use the DbSet<T>.AddOrUpdate() helper extension method
            //  to avoid creating duplicate seed data.

            context.News.AddOrUpdate(
                 new Model.News()
                 {
                     Title = "美國大城市房價太貴 年輕人靠「眾籌」買房",
                     Author = "佚名",
                     Content = "美國大城市房價太貴 年輕人靠「眾籌」買房",
                     CreateTime = DateTime.Now,
                     Amount = 0,
                 },
                 new Model.News()
                 {
                     Title = "血腥撲殺流浪狗太殘忍?那提高成本就是必須的代價",
                     Author = "佚名",
                     Content = "血腥撲殺流浪狗太殘忍?那提高成本就是必須的代價",
                     CreateTime = DateTime.Now,
                     Amount = 0,
                 },
                 new Model.News()
                 {
                     Title = "iPhone 8或9月6日釋出 售價或1100美元起",
                     Author = "網路",
                     Content = "iPhone 8或9月6日釋出 售價或1100美元起",
                     CreateTime = DateTime.Now,
                     Amount = 0,
                 }
                 );
        }
    }
}

3、資料庫上下文定義如下

using EFConcurrencyApp.Model;
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace EFConcurrencyApp.EF
{
    public class EFDbContext:DbContext
    {
        public EFDbContext()
            : base("name=AppConnection")
        {

        }

        public DbSet<News> News { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            // 設定表名和主鍵
            modelBuilder.Entity<News>().ToTable("News").HasKey(p => p.Id);
            base.OnModelCreating(modelBuilder);
        }
    }
}

4、實現EF的預設並行

先看一下EF預設是如何處理並行的,現在假設我們的應用程式要更新一個News的Amount值,那麼我們首先需要實現這兩個函數FindNews()和UpdateNews(),前者用於獲取指定的News,後者用於更新指定News。

Program類裡面定義的兩個方法如下:

static News FindNews(int id)
{
      using (var db = new EFDbContext())
      {
            return db.News.Find(id);
      }
}
static void UpdateNews(News news)
{
      using (var db = new EFDbContext())
      {
          db.Entry(news).State = EntityState.Modified;
          db.SaveChanges();
       }
}

下面我們實現這樣一個場景:有兩個使用者甲和乙都讀取了同一個News實體,然後這兩個使用者都嘗試更新這個實體的不同欄位,比如甲更新Title欄位,乙更新Author欄位,程式碼如下:

//1.使用者甲獲取id=1的新聞
var news1 = FindNews(1);
//2.使用者乙獲取id=1的新聞
var news2 = FindNews(1);
//3.使用者甲更新這個實體的新聞標題
news1.Title = news1.Title + "(更新)";
UpdateNews(news1);
//4.使用者乙更新這個實體的Amount
news2.Amount = 10m;
UpdateNews(news2);

上面的程式碼嘗試模擬了一種並行問題。現在,甲和乙兩個使用者都有相同的資料副本,然後嘗試更新相同的記錄。執行程式碼前,先看一下資料庫中的資料:

為了測試,在執行第四步時打一個斷點:

在斷點之後的程式碼執行之前,去資料庫看一下資料,可以看到使用者甲的更新已經產生作用了:

繼續執行程式碼,在看一下資料庫中的資料發生了什麼變化:

從上面的截圖可以看出,使用者乙的請求成功了,而使用者甲的更新丟失了。因此,從上面的程式碼不難看出,如果我們使用EF更新整條資料,那麼最後一個請求總會獲得勝利,也就是說:最後一次請求的更新會覆蓋之前所有請求的更新。

四、設計處理欄位級別並行的應用

接下來,我們會看到如何編寫處理欄位級別並行問題的應用程式碼。這是設計方式的應用思想是:只有更新的欄位才會在資料庫中進行更改。這樣就保證瞭如果多個使用者正在更新不同的欄位,所有的更改都可以持久化到資料庫。

實現這個的關鍵是讓該應用識別使用者正在請求更新的所有列,然後為該使用者有選擇地更新那些欄位。通過以下兩個方法來實現:

取資料的方法:該方法會給我們一個原始模型的克隆,只有使用者請求的屬性會更新為新值。

更新的方法:它會檢查原始請求模型的哪個屬性值已經發生更改,然後在資料庫中只更新那些值。

因此,首先需要建立一個簡單的方法,該方法需要模型屬性的值,然後會返回一個新的模型,該模型除了使用者嘗試更新的屬性以外,其他的屬性值都和原來的模型屬性值相同。方法定義如下:

static News GetUpdatedNews(int id, string title, string author, decimal amount, string content, DateTime createTime)
{
     return new News
     {
           Id = id,
           Title = title,
           Amount = amount,
           Author = author,
           Content = content,
           CreateTime = createTime,
      };
}

下一步,需要更改更新的方法。該更新方法會實現下面更新資料的演演算法:

1、根據Id從資料庫中檢索最新的模型值。

2、檢查原始模型和要更新的模型來找出更改屬性的列表。

3、只更新步驟2中檢索到的模型發生變化的屬性。

4、儲存更改。

更新方法定義如下:

static void UpdateNewsEnhanced(News originalNews, News newNews)
{
            using (var db = new EFDbContext())
            {
                //從資料庫中檢索最新的模型
                var news = db.News.Find(originalNews.Id);
                //接下來檢查使用者修改的每個屬性
                if (originalNews.Title != newNews.Title)
                {
                    //將新值更新到資料庫
                    news.Title = newNews.Title;
                }
                if (originalNews.Content != newNews.Content)
                {
                    //將新值更新到資料庫
                    news.Content = newNews.Content;
                }
                if (originalNews.CreateTime != newNews.CreateTime)
                {
                    //將新值更新到資料庫
                    news.CreateTime = newNews.CreateTime;
                }
                if (originalNews.Amount != newNews.Amount)
                {
                    //將新值更新到資料庫
                    news.Amount = newNews.Amount;
                }
                if (originalNews.Author != newNews.Author)
                {
                    //將新值更新到資料庫
                    news.Author = newNews.Author;
                }
                // 持久化到資料庫
                db.SaveChanges();
            }
}

執行程式碼前,先檢視資料庫中的資料:

然後執行主程式程式碼,在執行第四步時打個斷點:

再次檢視資料庫的資料,發現使用者甲的操作已經執行了:

繼續執行程式,再次檢視資料庫的資料,發現使用者乙的操作也執行了:

從上面的截圖看到,兩個使用者請求同一個實體的更新值都持久化到了資料庫中。因此,如果使用者更新不同的欄位,該程式可以有效地處理並行更新了。但是如果多個使用者同時更新相同的欄位,那麼這種方法仍然顯示的是最後一次請求的值。雖然這種方式減少了一些並行相關的問題,但是這種方法意味著我們必須寫大量程式碼來處理並行問題。後面我們會看到如何使用EF提供的機制來處理並行問題。

五、使用RowVersion實現並行

前面我們看到了EF預設如何處理並行(最後一次請求的資料更新成功),然後看到如果多個使用者嘗試更新不同的欄位時,如何設計應用處理這些問題。接下來,我們看一下當多個使用者更新相同的欄位時,使用EF如何處理欄位級更新。

EF讓我們指定欄位級並行,這樣如果一個使用者更新一個欄位的同時,該欄位已經被其他使用者更新過了,就會丟擲一個並行相關的異常。使用這種方法,當多個使用者嘗試更新相同的欄位時,我們就可以更有效地處理並行相關的問題。

如果我們為多個欄位使用了特定欄位的並行,那麼會降低應用效能,因為生成的SQL會更大,更加有效的方式就是使用RowVersion機制。RowVersion機制使用了一種資料庫功能,每當更新行的時候,就會建立一個新的行值。

給News實體類新增一個屬性:

1646467287
public byte[] RowVersion { get; set; }

在資料庫上下文中設定屬性:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
      // 設定表名和主鍵
      modelBuilder.Entity<News>().ToTable("News").HasKey(p => p.Id);
      // 設定屬性
      modelBuilder.Entity<News>().Property(d => d.RowVersion).IsRowVersion();
      base.OnModelCreating(modelBuilder);
}

刪除原先的資料庫,然後重新生成資料庫,資料庫模式變為:

檢視資料,RowVersion列顯示的是二進位制資料:

現在EF就會為並行控制追蹤RowVersion列值。接下來嘗試更新不同的列:

using (var context = new EFDbContext())
{
                var news = context.News.SingleOrDefault(p => p.Id == 1);
                Console.WriteLine(string.Format("標題:{0} 打賞金額:{1} ", news.Title, news.Amount.ToString("C")));
                context.Database.ExecuteSqlCommand(@"update news set 
                        amount = 229.95 where Id = @p0", news.Id);
                news.Amount = 239.95M;
                Console.WriteLine(string.Format("標題:{0} 打賞金額:{1} ", news.Title, news.Amount.ToString("C")));
                context.SaveChanges();
}

執行程式,會丟擲下面的異常:

從丟擲的異常資訊來看,很明顯是丟擲了和並行相關的異常DbUpdateConcurrencyException,其他資訊說明了自從實體載入以來,可能已經被修改或刪除了。

無論何時一個使用者嘗試更新一條已經被其他使用者更新的記錄,都會獲得異常DbUpdateConcurrencyException。

當實現並行時,我們總要編寫例外處理的程式碼,給使用者展示一個更友好的描述資訊。上面的程式碼加上例外處理機制後修改如下:

using (var context = new EFDbContext())
{
      var news = context.News.SingleOrDefault(p => p.Id == 1);
      Console.WriteLine(string.Format("標題:{0} 打賞金額:{1} ", news.Title, news.Amount.ToString("C")));
      context.Database.ExecuteSqlCommand(string.Format(@"update News set 
                        Amount = 229.95 where Id = {0}", news.Id));
      news.Amount = 239.95M;
      Console.WriteLine(string.Format("標題:{0} 打賞金額:{1} ", news.Title, news.Amount.ToString("C")));

      try
      {
            context.SaveChanges();
      }
      catch (DbUpdateConcurrencyException ex)
      {
            Console.WriteLine(string.Format("並行異常:{0}", ex.Message));
      }
      catch (Exception ex)
      {
            Console.WriteLine(string.Format("普通異常:{0}", ex.Message));
      }
}

此時,我們應該使用當前的資料庫值更新資料,然後重新更改。作為開發者,如果我們想要協助使用者的話,我們可以使用EF的DbEntityEntry類獲取當前的資料庫值。

using (var context = new EFDbContext())
{
      var news = context.News.SingleOrDefault(p => p.Id == 1);
      Console.WriteLine(string.Format("標題:{0} 打賞金額:{1} ", news.Title, news.Amount.ToString("C")));
              
context.Database.ExecuteSqlCommand(string.Format(@"update News set 
       Amount = 229.95 where Id = {0}", news.Id));
       news.Amount = 239.95M;
       Console.WriteLine(string.Format("標題:{0} 打賞金額:{1} ", news.Title, news.Amount.ToString("C")));       try
       {
         context.SaveChanges();
       }
       catch (DbUpdateConcurrencyException ex)
       {
          // 使用這段程式碼會將Amount更新為239.95
          var postEntry = context.Entry(news);
          postEntry.OriginalValues.SetValues(postEntry.GetDatabaseValues());
          context.SaveChanges();
        }
        catch (Exception ex)
        {
           Console.WriteLine(string.Format("普通異常:{0}", ex.Message));
        }
}

範例程式碼下載地址:點此下載

到此這篇關於Entity Framework管理並行的文章就介紹到這了。希望對大家的學習有所幫助,也希望大家多多支援it145.com。


IT145.com E-mail:sddin#qq.com