EntityFramework Core解决并发详解

前言

对过年已经无感,不过还是有很多闲暇时间来学学东西和多陪陪爸妈,这一点是极好的,好了,本节我们来讲讲EntityFramework Core中的并发问题。

话题(EntityFramework Core并发)

对于并发问题这个话题相信大家并不陌生,当数据量比较大时这个时候我们就需要考虑并发,对于并发涉及到的内容也比较多,在EF Core中我们将并发分为几个小节来陈述,让大家看起来也不太累,也容易接受,我们由浅入深。首先我们看下给出的Blog实体类。

    public class Blog : IEntityBase
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Url { get; set; }
        public ICollection<Post> Posts { get; set; }
    }

对于在VS2015中依赖注入仓储我们就不再叙述,比较简单,我们看下控制器中的两个方法,一个是渲染数据,一个是更新数据的方法,如下:

    public class HomeController : Controller
    {
        private IBlogRepository _blogRepository;
        public HomeController(IBlogRepository blogRepository)
        {
            _blogRepository = blogRepository;
        }
        public IActionResult Index()
        {
            var blog = _blogRepository.GetSingle(d => d.Id == 1);
            return View(blog);
        }

        [HttpPost]
        public IActionResult Index(Blog obj)
        {
            try
            {
                _blogRepository.Update(obj);
                _blogRepository.Commit();
            }
            catch (Exception ex)
            {
                ModelState.AddModelError("", ex.Message);
            }
            return View(obj);
        }
    }

视图渲染数据如下:

@using StudyEFCore.Model.Entities
@model Blog
<html>
<head>
    <title></title>
</head>
<body>
    @using (Html.BeginForm("Index", "Home", FormMethod.Post))
    {
        <table border="1" cellpadding="10">
            <tr>
                <td>博客ID :</td>
                <td>
                    @Html.TextBoxFor(m => m.Id,
       new { @readonly = "readonly" })
            </td>
        </tr>
        <tr>
            <td>博客名称 :</td>
            <td>@Html.TextBoxFor(m => m.Name)</td>
        </tr>
        <tr>
            <td>博客地址:</td>
            <td>@Html.TextBoxFor(m => m.Url)</td>
        </tr>
        <tr>
            <td colspan="2">
                <input type="submit" value="更新" />
            </td>
        </tr>
    </table>
    }
    @Html.ValidationSummary()
</body>
</html>

最终在页面上渲染的数据如下:

接下来我们演示下如何引起并发问题,如下:

上述我们通过在视图页面更新值后然后在SaveChanges之前打断点,然后我们在数据库中改变其值,再来SaveChanges此时会报异常,错误信息如下:

Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions.

因为在我们页面上改变其值后未进行SaveChanges,但是此时我们修改了Name的值,接着再来SaveChanges,此时报上述错误也就是我们本节所说的并发问题。既然出现了这样的问题,那么我们在EF Core中该如何解决出现的并发问题呢?在这里我们有两种方式,我们一一来陈述。

EF Core并发解决方案一(并发Token)

既然要讲并发Token,那么在此之前我们需要讲讲并发Token到底是怎样工作的,当我们对属性标识为并发Token,当我们从数据库中加载其值时,此时对应的属性的并发Token也就通过上下文而分配,当对分配的并发Token属性的相同的值进行了更新或者删除,此时会强制该属性的并发Token去进行检测,它会去检测影响的行数量,如果并发已经匹配到了,然后一行将被更新到,如果该值在数据库中已经被更新,那么将没有数据行会被更新。对于更新或者删除通过在WHERE条件上包括并发Token。接下来我们对要更新的Name将其设置为并发Token,如下:

    public class BlogMap : EntityMappingConfiguration<Blog>
    {
        public override void Map(EntityTypeBuilder<Blog> b)
        {
            b.ToTable("Blog");
            b.HasKey(k => k.Id);
            b.Property(p => p.Name).IsConcurrencyToken();
            b.Property(p => p.Url);
            b.HasMany(p => p.Posts).WithOne(p => p.Blog).HasForeignKey(p => p.BlogId);
        }
    }

当我们进行如上设置后再来迁移更新模型,最终还是会抛出如下异常:

Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions.

接下来我们再来看看解决并发而设置行版本的情况。

EF Core并发解决方案二(行版本)

当我们在插入或者更新时都会产生一个新的timestamp,这个属性也会被当做一个并发Token来对待,它会确保当我们更新值时但是其值已经被修改过时一定会如上所述抛出异常。那么怎么使用行版本呢,(我们只讲Fluent API关于Data Annotations请自行查找资料)在实体中定义如下属性:

 public byte[] RowVersion { get; set; }

接着对该属性进行如下配置。

b.Property(p => p.RowVersion).IsConcurrencyToken().ValueGeneratedOnAddOrUpdate();

当我们再次进行如上演示时肯定会抛出同样的异常信息。 

上述两种从本质上都未能解决在EF Core中的并发问题只是做了基础的铺垫,那么我们到底该如何做才能解决并发问题呢,请继续往下看。

解析EF Core并发冲突

我们通过三种设置来解析EF Core中的并发冲突,如下:

当前值(Current values):试图将当前修改的值写入到到数据库。

原始值(Original values):在未做任何修改时的需要从数据库中检索到的值。

数据值(Database values):当前保存在数据库中的值。

由于并发会抛出异常,所以我们需要 在SaveChanges时在并发冲突所产生的异常中来进行解决,并发异常呈现在 DbUpdateConcurrencyException 类中,我们只需要在此并发异常类解决即可。比如上述我们需要修改Name的值,我们做了基础的铺垫,设置了并发Token。但是还是会引发并发异常,未能解决问题,这个只是解决并发异常的前提,由于我们利用的仓储来操作数据,但是并发异常会利用到EF上下文,所以我们额外定义接口,直接通过上下文来操作,如下我们定义一个接口

    public interface IBlogRepository : IEntityBaseRepository<Blog>
    {
        void UpdateBlog(Blog blog);
    }

解决并发异常通过EF上下文来操作。

     public class BlogRepository : EntityBaseRepository<Blog>,
        IBlogRepository
    {
        private EFCoreContext _efCoreContext;
        public BlogRepository(EFCoreContext efCoreContext) : base(efCoreContext)
        {
            _efCoreContext = efCoreContext;
        }

        public void UpdateBlog(Blog blog)
        {
            try
            {
                _efCoreContext.Set<Blog>().Update(blog);
                _efCoreContext.SaveChanges();
            }
            catch (DbUpdateConcurrencyException ex)
            {
                foreach (var entry in ex.Entries)
                {
                    if (entry.Entity is Blog)
                    {
                        var databaseEntity = _efCoreContext.Set<Blog>().AsNoTracking().Single(p => p.Id == ((Blog)entry.Entity).Id);
                        var databaseEntry = _efCoreContext.Entry(databaseEntity);

                        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;

                            // TODO: Logic to decide which value should be written to database
                            var propertyName = property.Name;
                            if (propertyName == "Name")
                            {
                                // Update original values to
                                entry.Property(property.Name).OriginalValue = databaseEntry.Property(property.Name).CurrentValue;
                                break;
                            }
                        }
                    }
                    else
                    {
                        throw new NotSupportedException("Don't know how to handle concurrency conflicts for " + entry.Metadata.Name);
                    }
                }

                // Retry the save operation
                _efCoreContext.SaveChanges();
            }
        }
    }

上述则是通用解决并发异常的办法,我们只是注意上述表明的TODO逻辑,我们需要得到并发的属性,然后再来更新其值即可,我们对于Name会产生并发,所以遍历实体属性时获取到Name,然后更新其值即可,简单粗暴,完胜。我们看如下演示。

上述我们将Name修改为efcoreefcore,在SaveChanges前修改数据库中的Name,接着再来进行SaveChanges时,此时肯定会走并发异常,我们在并发异常中进行处理,最终我们能够很清楚的看到最终数据库中的Name更新为efcoreefcore,我们在最后重试一次在一定程度上可以保证能够解决并发。

总结

本节我们比较详细的讲解了EntityFramework Core中的并发问题以及该如何解决,到这里算是基本结束,我才发现在项目当中未经测试我居然用错了,明天去修改修改,这里算是一个稍微详细的讲解吧,如果进行压力测试不知道结果会怎样,后续进行压力测试若有进一步的进展再来完善,到时再来更新EF Core并发后续,好了,不早了,晚安。

时间: 2024-07-31 02:22:29

EntityFramework Core解决并发详解的相关文章

EntityFramework Core映射关系详解

前言 Hello,开始回归开始每周更新一到两篇博客,本节我们回归下EF Core基础,来讲述EF Core中到底是如何映射的,废话少说,我们开始. One-Many Relationship(一对多关系) 首先我们从最简单的一对多关系说起,我们给出需要映射的两个类,一个是Blog,另外一个则是Post,如下: public class Blog { public int Id { get; set; } public int Count { get; set; } public string N

《NoSQL权威指南》——1.4 悲观并发详解

1.4 悲观并发详解 悲观并发控制假定冲突是预料之中的情况,必须警惕.在关系数据库管理系统(relational database management system,RDBMS)中最流行的模型是基于加锁的.锁是一种允许一个用户会话对资源的访问同时保持或限制其他会话对同一资源的访问的装置.每个会话可以针对资源获得对应的锁,对资源进行修改,然后在数据库中提交(COMMIT)或回滚(ROLLBACK)相应的操作.COMMIT语句将修改持久保存,ROLLBACK语句将数据库恢复到会话之前的状态.如果修

Android DaggerActivityComponent错误解决办法详解

Android DaggerActivityComponent错误解决办法详解 在使用dagger2的过程中,如果修改了某个类的内容,第一次编译运行时总会报错:错误: 找不到符号 符号: 类 DaggerActivityComponent 位置: 程序包 com--的错误,然后再重新编译一次,才会正常运行,经过仔细的检查终于找到问题的根源: 错误的原因是build.gradle(Module:app)引入'com.google.dagger:dagger-compiler:2.0.2'使用的是c

在Parallel中使用DbSet.Add()发现的一系列多线程问题和解决思路详解_C#教程

发现问题 需求很简单,大致就是要批量往数据库写数据,于是打算用Parallel并行的方式写入,希望能利用计算机多核特性加快程序执行速度.想的很美好,于是快速撸了类似下面的一串代码: using (var db = new SmsEntities()) { Parallel.For(0, 1000, (i) => { db.MemberCard.Add(new MemberCard() { CardNo = "NO_" + i.ToString(), Banlance = 0, C

MySQL redo死锁问题排查及解决过程详解

问题背景   周一上班,首先向同事了解了一下上周的测试情况,被告知在多实例场景下 MySQL Server hang 住,无法测试下去,原生版本不存在这个问题,而新版本上出现了这个问题,不禁心头一颤,心中不禁感到奇怪,还好现场环境还在,为排查问题提供了一个好的环境,随即便投入到紧张的问题排查过程当中.问题实例表现如下: 并发量为 384 的时候出现的问题: MySQL 服务器无法执行事务相关的语句,即使简单的 select 语句也无法执行: 所有线程处于等待状态,无法 KILL. 现场环境的收集

求子数组最大和的解决方法详解_C 语言

题目:输入一个整形数组,数组里有正数也有负数.数组中连续的一个或多个整数组成一个子数组,每个子数组都有一个和.求所有子数组的和的最大值.要求时间复杂度为O(n). 例如输入的数组为1, -2, 3, 10, -4, 7, 2, -5,和最大的子数组为3, 10, -4, 7, 2,因此输出为该子数组的和18.如果不考虑时间复杂度,我们可以枚举出所有子数组并求出他们的和.不过非常遗憾的是,由于长度为n的数组有O(n2)个子数组:而且求一个长度为n的数组的和的时间复杂度为O(n).因此这种思路的时间

php gd库中文乱码的产生与解决方法详解

在php jpgraph安装教程之验证php环境是否支持jpgraph安装方法的php教程中我提到了gd库的验证,对于jpgraph中文使用者来说使用jpgraph时不产生中文乱码是非常必要的,而gd库作为jpgraph类库运行在php安装环境下的基础,保证使用gd库时不出现中文乱码是非常必要的,之前我在验证gd库对jpgraph类库的支持时也出现使用gd函数imagettftext函数出现中文乱码的情况,下面介绍下使用php gd库出现中文乱码的原因与解决方法. php gd库产生中文乱码的原

局域网共享错误解决方法详解

问题1.连接了一个共享后,再次连接时候提示错误. 或者共享机器更改了密码,需要重新连接,重新输入新的账号密码,不需要重启计算机,的解决方法. --------------------------- 192.6.6.6e$ --------------------------- 192.6.6.6e$ 不允许一个用户使用一个以上用户名与一个服务器或共享资源的多重连接.中断与此服务器或共享资源的所有连接,然后再试一次... --------------------------- 确定 -------

手机用safari无法下载此文件原因及解决方法详解

ipad或者iphone上网,不管下载什么都提示safari无法下载此文件要怎么解决呢?其实原因是因为苹果设备自带浏览器safari不支持下载. 手机用safari无法下载此文件原因及解决方法 : 如果你要下载到AppStore里面买个iDownload软件或者越狱在cydia上安装个safari download plugin插件就可以下载了.