Entity Framework管理并發(fā)
理解并發(fā)
并發(fā)管理解決的是允許多個實體同時更新,實際上這意味著允許多個用戶同時在相同的數(shù)據(jù)上執(zhí)行多個數(shù)據(jù)庫操作。并發(fā)是在一個數(shù)據(jù)庫上管理多個操作的一種方式,同時遵守了數(shù)據(jù)庫操作的ACID屬性(原子性、一致性、隔離性和持久性)。
想象一下下面幾種可能發(fā)生并發(fā)的場景:
1、用戶甲和乙都嘗試修改相同的實體。
2、用戶甲和乙都嘗試刪除相同的實體。
3、用戶甲正在嘗試修改一個實體時,用戶乙已經(jīng)刪除了該實體。
4、用戶甲已經(jīng)請求讀取一個實體,用戶乙讀完該實體之后更新了它。
這些場景可能會潛在地產(chǎn)生錯誤的數(shù)據(jù),試想,成百上千的用戶同時嘗試操作一個相同的實體,這種并發(fā)問題將會對系統(tǒng)帶來更大的影響。
在處理與并發(fā)相關的問題時,一般有以下兩種方法:
1、樂觀并發(fā):無論何時從數(shù)據(jù)庫請求數(shù)據(jù),數(shù)據(jù)都會被讀取并保存到應用內(nèi)存中。數(shù)據(jù)庫級別沒有放置任何顯示鎖。數(shù)據(jù)操作會按照數(shù)據(jù)層接收到的順序執(zhí)行。
2、悲觀并發(fā):無論何時從數(shù)據(jù)庫請求數(shù)據(jù),數(shù)據(jù)都會被讀取,然后該數(shù)據(jù)上就會加鎖,因此沒有人能訪問該數(shù)據(jù)。這會降低并發(fā)相關問題的機率,缺點是加鎖是一個昂貴的操作,會降低整個應用程序的性能。
一、理解樂觀并發(fā)
前面提到,在樂觀并發(fā)中,無論何時從數(shù)據(jù)庫請求數(shù)據(jù),數(shù)據(jù)都會被讀取并保存到應用內(nèi)存中。數(shù)據(jù)庫級別沒有放置任何顯式鎖。因為這種方法沒有添加顯式鎖,所以比悲觀并發(fā)更具擴展性和靈活性。使用樂觀并發(fā),重點是如果發(fā)生了任何沖突,應用程序要親自處理它們。最重要的是:使用樂觀并發(fā)控制時,在應用中要有一個沖突解決策略,要讓應用程序的用戶知道他們的修改是否因為沖突的緣故沒有持久化。樂觀并發(fā)本質上是允許沖突發(fā)生,然后以一種適當?shù)姆绞浇鉀Q該沖突。
下面是處理沖突的策略例子。
1、忽略沖突/強制更新
這種策略是讓所有的用戶更改相同的數(shù)據(jù)集,然后所有的修改都會經(jīng)過數(shù)據(jù)庫,這就意味著數(shù)據(jù)庫會顯示最后一次更新的值。這種策略會導致潛在的數(shù)據(jù)丟失,因為許多用戶的更改數(shù)據(jù)都丟失了,只有最后一個用戶的更改是可見的。
2、部分更新
在這種情況中,我們也允許所有的更改,但是不會更新完整的行,只有特定用戶擁有的列更新了。這就意味著,如果兩個用戶更新相同的記錄但卻不同的列,那么這兩個更新都會成功,而且來自這兩個用戶的更改都是可見的。
3、警告/詢問用戶
當一個用戶嘗試更新一個記錄時,但是該記錄自從他讀取之后已經(jīng)被其他用戶更改了,這時應用程序就會警告該用戶該數(shù)據(jù)已經(jīng)被其他用戶更改了,然后詢問他是否仍然要重寫該數(shù)據(jù)還是首先檢查已經(jīng)更新的數(shù)據(jù)。
4、拒絕更改
當一個用戶嘗試更新一個記錄時,但是該記錄自從他讀取之后已經(jīng)被其他用戶更改了,此時告訴該用戶不允許更新該數(shù)據(jù),因為數(shù)據(jù)已經(jīng)被其他用戶更新了。
二、理解悲觀并發(fā)
悲觀并發(fā)正好和樂觀并發(fā)相反,悲觀并發(fā)的目標是永遠不讓任何沖突發(fā)生。這是通過在使用記錄之前就在記錄上放置顯式鎖實現(xiàn)的。數(shù)據(jù)庫記錄上可以得到兩種類型的鎖:
只讀鎖
更新鎖。
當把只讀鎖放到記錄上時,應用程序只能讀取該記錄。如果應用程序要更新該記錄,它必須要獲取到該記錄上的更新鎖。如果記錄上加了只讀鎖,那么該記錄仍然能夠被想要只讀鎖的請求使用。然而,如果需要更新鎖,該請求必須等到所有的只讀鎖釋放。同樣,如果記錄上加了更新鎖,那么其他的請求不能再在這個記錄上加鎖,該請求必須等到已存在的更新鎖釋放才能加鎖。
從前面的描述中,似乎悲觀并發(fā)能解決所有跟并發(fā)相關的問題,因為我們不必在應用中處理這些問題。然而,事實上并不是這樣的。在使用悲觀并發(fā)管理之前,我們需要記住,使用悲觀并發(fā)有很多問題和開銷。下面是使用悲觀并發(fā)面臨的一些問題:
應用程序必須管理每個操作正在獲取的所有鎖。
加鎖機制的內(nèi)存需求會降低應用性能。
多個請求互相等待需要的鎖,會增加死鎖的可能性。由于這些原因,EF不直接支持悲觀并發(fā)。如果想使用悲觀并發(fā)的話,我們可以自定義數(shù)據(jù)庫訪問代碼。此外,當使用悲觀并發(fā)時,LINQ to Entities不會正確工作。
三、使用EF實現(xiàn)樂觀并發(fā)
使用EF實現(xiàn)樂觀并發(fā)有很多方法,接下來我們就看一下這些方法。
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、使用數(shù)據(jù)遷移的方式生成數(shù)據(jù)庫,并填充種子數(shù)據(jù)。
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日發(fā)布 售價或1100美元起",
Author = "網(wǎng)絡",
Content = "iPhone 8或9月6日發(fā)布 售價或1100美元起",
CreateTime = DateTime.Now,
Amount = 0,
}
);
}
}
}3、數(shù)據(jù)庫上下文定義如下
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、實現(xiàn)EF的默認并發(fā)
先看一下EF默認是如何處理并發(fā)的,現(xiàn)在假設我們的應用程序要更新一個News的Amount值,那么我們首先需要實現(xiàn)這兩個函數(shù)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();
}
}下面我們實現(xiàn)這樣一個場景:有兩個用戶甲和乙都讀取了同一個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);
上面的代碼嘗試模擬了一種并發(fā)問題。現(xiàn)在,甲和乙兩個用戶都有相同的數(shù)據(jù)副本,然后嘗試更新相同的記錄。執(zhí)行代碼前,先看一下數(shù)據(jù)庫中的數(shù)據(jù):

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

在斷點之后的代碼執(zhí)行之前,去數(shù)據(jù)庫看一下數(shù)據(jù),可以看到用戶甲的更新已經(jīng)產(chǎn)生作用了:

繼續(xù)執(zhí)行代碼,在看一下數(shù)據(jù)庫中的數(shù)據(jù)發(fā)生了什么變化:

從上面的截圖可以看出,用戶乙的請求成功了,而用戶甲的更新丟失了。因此,從上面的代碼不難看出,如果我們使用EF更新整條數(shù)據(jù),那么最后一個請求總會獲得勝利,也就是說:最后一次請求的更新會覆蓋之前所有請求的更新。
四、設計處理字段級別并發(fā)的應用
接下來,我們會看到如何編寫處理字段級別并發(fā)問題的應用代碼。這是設計方式的應用思想是:只有更新的字段才會在數(shù)據(jù)庫中進行更改。這樣就保證了如果多個用戶正在更新不同的字段,所有的更改都可以持久化到數(shù)據(jù)庫。
實現(xiàn)這個的關鍵是讓該應用識別用戶正在請求更新的所有列,然后為該用戶有選擇地更新那些字段。通過以下兩個方法來實現(xiàn):
取數(shù)據(jù)的方法:該方法會給我們一個原始模型的克隆,只有用戶請求的屬性會更新為新值。
更新的方法:它會檢查原始請求模型的哪個屬性值已經(jīng)發(fā)生更改,然后在數(shù)據(jù)庫中只更新那些值。
因此,首先需要創(chuàng)建一個簡單的方法,該方法需要模型屬性的值,然后會返回一個新的模型,該模型除了用戶嘗試更新的屬性以外,其他的屬性值都和原來的模型屬性值相同。方法定義如下:
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,
};
}下一步,需要更改更新的方法。該更新方法會實現(xiàn)下面更新數(shù)據(jù)的算法:
1、根據(jù)Id從數(shù)據(jù)庫中檢索最新的模型值。
2、檢查原始模型和要更新的模型來找出更改屬性的列表。
3、只更新步驟2中檢索到的模型發(fā)生變化的屬性。
4、保存更改。
更新方法定義如下:
static void UpdateNewsEnhanced(News originalNews, News newNews)
{
using (var db = new EFDbContext())
{
//從數(shù)據(jù)庫中檢索最新的模型
var news = db.News.Find(originalNews.Id);
//接下來檢查用戶修改的每個屬性
if (originalNews.Title != newNews.Title)
{
//將新值更新到數(shù)據(jù)庫
news.Title = newNews.Title;
}
if (originalNews.Content != newNews.Content)
{
//將新值更新到數(shù)據(jù)庫
news.Content = newNews.Content;
}
if (originalNews.CreateTime != newNews.CreateTime)
{
//將新值更新到數(shù)據(jù)庫
news.CreateTime = newNews.CreateTime;
}
if (originalNews.Amount != newNews.Amount)
{
//將新值更新到數(shù)據(jù)庫
news.Amount = newNews.Amount;
}
if (originalNews.Author != newNews.Author)
{
//將新值更新到數(shù)據(jù)庫
news.Author = newNews.Author;
}
// 持久化到數(shù)據(jù)庫
db.SaveChanges();
}
}運行代碼前,先查看數(shù)據(jù)庫中的數(shù)據(jù):

然后執(zhí)行主程序代碼,在執(zhí)行第四步時打個斷點:

再次查看數(shù)據(jù)庫的數(shù)據(jù),發(fā)現(xiàn)用戶甲的操作已經(jīng)執(zhí)行了:


繼續(xù)運行程序,再次查看數(shù)據(jù)庫的數(shù)據(jù),發(fā)現(xiàn)用戶乙的操作也執(zhí)行了:


從上面的截圖看到,兩個用戶請求同一個實體的更新值都持久化到了數(shù)據(jù)庫中。因此,如果用戶更新不同的字段,該程序可以有效地處理并發(fā)更新了。但是如果多個用戶同時更新相同的字段,那么這種方法仍然顯示的是最后一次請求的值。雖然這種方式減少了一些并發(fā)相關的問題,但是這種方法意味著我們必須寫大量代碼來處理并發(fā)問題。后面我們會看到如何使用EF提供的機制來處理并發(fā)問題。
五、使用RowVersion實現(xiàn)并發(fā)
前面我們看到了EF默認如何處理并發(fā)(最后一次請求的數(shù)據(jù)更新成功),然后看到如果多個用戶嘗試更新不同的字段時,如何設計應用處理這些問題。接下來,我們看一下當多個用戶更新相同的字段時,使用EF如何處理字段級更新。
EF讓我們指定字段級并發(fā),這樣如果一個用戶更新一個字段的同時,該字段已經(jīng)被其他用戶更新過了,就會拋出一個并發(fā)相關的異常。使用這種方法,當多個用戶嘗試更新相同的字段時,我們就可以更有效地處理并發(fā)相關的問題。
如果我們?yōu)槎鄠€字段使用了特定字段的并發(fā),那么會降低應用性能,因為生成的SQL會更大,更加有效的方式就是使用RowVersion機制。RowVersion機制使用了一種數(shù)據(jù)庫功能,每當更新行的時候,就會創(chuàng)建一個新的行值。
給News實體類添加一個屬性:
[Timestamp]
public byte[] RowVersion { get; set; }在數(shù)據(jù)庫上下文中配置屬性:
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);
}刪除原先的數(shù)據(jù)庫,然后重新生成數(shù)據(jù)庫,數(shù)據(jù)庫模式變?yōu)椋?/p>

查看數(shù)據(jù),RowVersion列顯示的是二進制數(shù)據(jù):

現(xiàn)在EF就會為并發(fā)控制追蹤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();
}運行程序,會拋出下面的異常:


從拋出的異常信息來看,很明顯是拋出了和并發(fā)相關的異常DbUpdateConcurrencyException,其他信息說明了自從實體加載以來,可能已經(jīng)被修改或刪除了。
無論何時一個用戶嘗試更新一條已經(jīng)被其他用戶更新的記錄,都會獲得異常DbUpdateConcurrencyException。
當實現(xiàn)并發(fā)時,我們總要編寫異常處理的代碼,給用戶展示一個更友好的描述信息。上面的代碼加上異常處理機制后修改如下:
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("并發(fā)異常:{0}", ex.Message));
}
catch (Exception ex)
{
Console.WriteLine(string.Format("普通異常:{0}", ex.Message));
}
}此時,我們應該使用當前的數(shù)據(jù)庫值更新數(shù)據(jù),然后重新更改。作為開發(fā)者,如果我們想要協(xié)助用戶的話,我們可以使用EF的DbEntityEntry類獲取當前的數(shù)據(jù)庫值。
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管理并發(fā)的文章就介紹到這了。希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
- Entity Framework使用Code First模式管理存儲過程
- Entity Framework使用Code First模式管理視圖
- Entity Framework加載控制Loading Entities
- Entity Framework使用LINQ操作實體
- Entity?Framework使用Code?First的實體繼承模式
- Entity Framework使用Code First模式管理數(shù)據(jù)庫
- Entity Framework表拆分為多個實體
- Entity?Framework管理一對二實體關系
- Entity?Framework管理一對一實體關系
- Entity?Framework實體拆分多個表
相關文章
ASP.NET MVC @Helper輔助方法和@functons自定義函數(shù)的使用方法
本文主要介紹ASP.NET MVC中使用@Helper和@functons自定義一些代碼片段,方便視圖調(diào)用,從而達到減少重復代碼,快速開發(fā)的目的,希望對大家有所幫助。2016-04-04
asp.net AjaxControlToolKit--TabContainer控件的介紹
ModalPopup控件允許一個asp頁面的部分內(nèi)容以對話框的模式顯示給用戶,同時會限制用戶于頁面的其他部分交互。對話框顯示的內(nèi)容可以是一個層級,這個層級的背景可以使用戶自定義的格式,簡單的理解好比是一個對話框彈出來后,主頁面會顯示灰色,且不可操作。2009-06-06
HTTP協(xié)議下用Web Service上傳大文件的解決方案
HTTP協(xié)議下用Web Service上傳大文件的解決方案...2007-04-04
Convert.ToInt32與Int32.Parse區(qū)別及Int32.TryParse
2個方法都可以把string轉換為int,那么他們有什么區(qū)別?什么時候該用什么?性能如何。 其實在2.0里還有Int32.TryParse也實現(xiàn)了同樣的效果。2009-01-01
ASP.NET延遲調(diào)用或多次調(diào)用第三方Web?API服務
這篇文章介紹了ASP.NET延遲調(diào)用或多次調(diào)用第三方Web?API服務的方法,文中通過示例代碼介紹的非常詳細。對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-10-10
asp.net html控件的File控件實現(xiàn)多文件上傳實例分享
asp.net中html控件的File控件實現(xiàn)多文件上傳簡單實例,開發(fā)工具vs2010使用c#語言,感興趣的朋友可以了解下,必定是多文件上傳值得學習,或許本文所提供的知識點對你有所幫助2013-02-02

