运维开发网

EntityFramework管理并发性

运维开发网 https://www.qedev.com 2022-08-20 17:42 出处:网络
这篇文章介绍了EntityFramework管理实现并发的方法,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下 理解并发

这篇文章介绍了EntityFramework管理实现并发的方法,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下 理解并发

并发管理解决了允许多个实体同时更新的问题,实际上就是允许多个用户同时对同一数据执行多个数据库操作。并发是一种管理数据库上多个操作的方法,同时观察数据库操作的ACID属性(原子性、一致性、隔离性和持久性)。

想象以下可能发生并发的场景:

1.用户A和B都试图修改同一个实体。

2.用户A和B都试图删除同一个实体。

3.当用户A试图修改一个实体时,用户B已经删除了该实体。

4.用户A请求读取一个实体,用户B在读取该实体后更新了它。

这些情况可能会产生错误的数据。想象一下,数百个用户试图同时操作同一个实体。这个并发问题对系统的影响会更大。

在处理与并发相关的问题时,一般有以下两种方法:

1.开放式并发:每当从数据库请求数据时,数据将被读取并保存到应用程序内存中。数据库级别没有显示锁。数据将按照数据层接收的顺序执行。

2.悲观并发:每当从数据库中请求数据时,它将被读取,然后被锁定,因此没有人能够访问它。这将降低并发相关问题的概率。缺点是锁定是一个开销很大的操作,会降低整个应用程序的性能。

一、理解乐观并发

如前所述,在开放式并发中,每当从数据库请求数据时,数据将被读取并保存到应用程序内存中。没有在数据库级别放置显式锁。因为这种方法不添加显式锁,所以它比悲观并发更具可扩展性和灵活性。使用开放式并发,关键是如果有任何冲突,应用程序应该自己处理它们。最重要的是,当使用乐观并发控制时,应用程序中应该有一个冲突解决策略,以便应用程序的用户可以知道他们的修改是否因为冲突而不持久。乐观主义本质上是允许冲突发生,然后以适当的方式解决它们。

以下是处理冲突的策略示例。

1.忽略冲突/强制更新

这种策略是让所有用户更改同一个数据集,然后所有的更改都会经过数据库,也就是说数据库会显示最后更新的值。这种策略会导致潜在的数据丢失,因为很多用户的更改数据都丢失了,只有最后一个用户的更改是可见的。

2.部分更新

在这种情况下,我们也允许所有的更改,但是不会更新完整的行,只会更新特定用户拥有的列。这意味着如果两个用户更新相同的记录但不同的列,那么两个更新都将成功,并且两个用户的更改都将可见。

3.警告/询问用户

当用户试图更新一个记录,但是该记录在他读取后被其他用户更改了,应用程序将警告用户数据已经被其他用户更改,然后询问他是否仍然想要重写数据或者首先检查更新的数据。

4.拒绝改变

当用户试图更新一条记录,但该记录在他读取后已被其他用户更改时,告诉该用户不允许更新该数据,因为该数据已被其他用户更新。

二、理解悲观并发

悲观并发正好与乐观并发相反。悲观并发的目标是永远不让任何冲突发生。这是通过在使用记录之前对其进行显式锁定来实现的。可以在数据库记录上获得两种类型的锁:

只读锁

更新锁。

当在记录上放置只读锁时,应用程序只能读取该记录。如果应用程序想要更新记录,它必须获取记录上的更新锁。如果一个只读锁被附加到一个记录,该记录仍然可以被需要一个只读锁的请求使用。但是,如果需要更新锁,请求必须等到所有只读锁都被释放。类似地,如果一个更新锁被添加到一个记录,其他请求不能锁定该记录,并且该请求必须等到现有的更新锁被释放。

从前面的描述来看,悲观并发似乎可以解决所有与并发相关的问题,因为我们在应用中不必处理这些问题。然而,事实上并非如此。在使用悲观并发管理之前,我们需要记住,使用悲观并发有许多问题和开销。下面是使用悲观并发面临的一些问题:

应用程序必须管理每个操作获取的所有锁。

锁定机制的内存需求会降低应用程序的性能。

等待所需锁的多个请求将增加死锁的可能性。由于这些原因,EF不直接支持悲观并发。如果你想使用悲观并发,我们可以定制数据库访问代码。此外,当使用悲观并发时,实体的LINQ将无法正常工作。

三、使用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 : DbMigrationsConfigurationlt;EFConcurrencyApp.EF.EFDbContextgt; { 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 DbSetlt;Tgt;.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 DbSetlt;Newsgt; News { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { // 设置表名和主键 modelBuilder.Entitylt;Newsgt;().ToTable("News").HasKey(p =gt; p.Id); base.OnModelCreating(modelBuilder); } }}

4.实现EF的默认并发

我们来看看默认情况下EF是如何处理并发的。现在假设我们的应用程序要更新新闻的金额值,那么我们需要实现这两个函数,FindNews()和UpdateNews()。前者用于获取指定的新闻,后者用于更新指定的新闻。

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(); }}

我们来实现这样一个场景:两个用户A和B都阅读同一个新闻实体,然后两个用户都尝试更新这个实体的不同字段,比如A更新标题字段,B更新作者字段。代码如下:

//1.用户甲获取id=1的新闻var news1 = FindNews(1);//2.用户乙获取id=1的新闻var news2 = FindNews(1);//3.用户甲更新这个实体的新闻标题news1.Title = news1.Title + "(更新)";UpdateNews(news1);//4.用户乙更新这个实体的Amountnews2.Amount = 10m;UpdateNews(news2);

上面的代码试图模拟一个并发问题。现在,用户A和B都有相同的数据副本,然后尝试更新相同的记录。在执行代码之前,看一下数据库中的数据:


要进行测试,请在执行步骤4时创建一个断点:


在断点之后的代码执行之前,去数据库看一下数据。可以看到用户A的更新已经生效了:


继续执行代码,查看数据库中的数据发生了什么变化:


从上面的截图可以看出,用户B的请求成功,而用户A的更新丢失。所以从上面的代码不难看出,如果我们用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, };}

接下来,您需要更改更新的方法。update方法将实现以下算法来更新数据:

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(); }}

运行代码之前,请检查数据库中的数据:


然后执行主程序代码,在执行第四步的时候做一个断点:


再看数据库的数据,发现已经进行了用户A的操作:



继续运行程序,再次查看数据库的数据,发现用户B的操作也被执行了:



从上面的截图可以看出,两个用户请求的同一个实体的更新值被持久存储在数据库中。因此,如果用户更新不同的字段,程序可以有效地处理并发更新。但是,如果多个用户同时更新同一个字段,此方法仍会显示最后请求的值。虽然这种方法减少了一些并发相关的问题,但这意味着我们必须编写大量的代码来处理并发问题。稍后,我们将看到如何使用EF提供的机制来处理并发问题。

五、使用RowVersion实现并发

前面,我们看到了EF在默认情况下如何处理并发性(最后一次请求的数据更新是成功的),然后我们看到了如果多个用户试图更新不同的字段,如何设计一个应用程序来处理这些问题。接下来,我们来看看当多个用户更新同一个字段时,如何用EF处理字段级更新。

EF我们来指定字段级并发,这样如果一个用户同时更新一个字段,这个字段已经被其他用户更新了,就会抛出一个并发相关的异常。使用这种方法,当多个用户试图更新同一个字段时,我们可以更有效地处理与并发相关的问题。

如果我们对多个字段使用特定字段的并发,会降低应用性能,因为生成的SQL会更大,更有效的方式是使用RowVersion机制。RowVersion机制使用一个数据库函数,每当更新一行时,该函数都会创建一个新的行值。

向新闻实体类添加属性:

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

在数据库上下文中配置属性:

protected override void OnModelCreating(DbModelBuilder modelBuilder){ // 设置表名和主键 modelBuilder.Entitylt;Newsgt;().ToTable("News").HasKey(p =gt; p.Id); // 设置属性 modelBuilder.Entitylt;Newsgt;().Property(d =gt; d.RowVersion).IsRowVersion(); base.OnModelCreating(modelBuilder);}

删除原始数据库,然后重建数据库。数据库模式变为:


查看数据,RowVersion列显示二进制数据:


EF现在将为并发控制跟踪RowVersion列值。接下来,尝试更新不同的列:

using (var context = new EFDbContext()){ var news = context.News.SingleOrDefault(p =gt; 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 =gt; 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 =gt; 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)); }}

示例代码下载地址:单击此处下载

这就是这篇关于实体框架管理并发性的文章。希望对大家的学习有所帮助

0

精彩评论

暂无评论...
验证码 换一张
取 消