总目录:ASP.NET MVC5 及 EF6 学习笔记 - (目录整理)

上一篇:EF学习笔记(九):异步处理和存储过程

本篇原文链接:Handling Concurrency

Concurrency Conflicts 并发冲突

发生并发冲突很简单,一个用户点开一条数据进行编辑,另外一个用户同时也点开这条数据进行编辑,那么如果不处理并发的话,谁后提交修改保存的,谁的数据就会被记录,而前一个就被覆盖了;

如果在一些特定的应用中,这种并发冲突可以被接受的话,那么就不用花力气去特意处理并发;毕竟处理并发肯定会对性能有所影响。

Pessimistic Concurrency (Locking) 保守并发处理(锁)

如果应用需要预防在并发过程中数据丢失,那么一种方式就是采用数据库锁;这种方式称为保守并发处理。

这种就是原有的处理方式,要修改数据前,先给数据库表或者行加上锁,然后在这个事务处理完之前,不会释放这个锁,等处理完了再释放这个锁。

但这种方式应该是对一些特殊数据登记才会使用的,比如取流水号,多个用户都在取流水号,用一个表来登记当前流水号,那么取流水号过程肯定要锁住表,不然同时两个用户取到一样的流水号就出异常了。

而且有的数据库都没有提供这种处理机制。EF并没有提供这种方式的处理,所以本篇就不会讲这种处理方式。

Optimistic Concurrency 开放式并发处理

替代保守并发处理的方式就是开放式并发处理,开放式并发处理运行并发冲突发生,但是由用户选择适当的方式来继续;(是继续保存数据还是取消)

比如在出现以下情况:John打开网页编辑一个Department,修改预算为0, 而在点保存之前,Jone也打开网页编辑这个Department,把开始日期做了调整,然后John先点了保存,Jone之后点了保存;

在这种情况下,有以下几种选择:

1、跟踪用户具体修改了哪个属性,只对属性进行更新;当时也会出现,两个用户同时修改一个属性的问题;EF是否实现这种,需要看自己怎么写更新部分的代码;在Web应用中,这种方式不是很合适,需要保持大量状态数据,维护大量状态数据会影响程序性能,因为状态数据要么需要服务器资源,要么需要包含在页面本身(隐藏字段)或Cookie中;

2、如果不做任何并发处理,那么后保存的就直接覆盖前一个保存的数据,叫做: Client Wins or Last in Wins

3、最后一种就是,在后一个人点保存的时候,提示相应错误,告知其当前数据的状态,由其确认是否继续进行数据更新,这叫做:Store Wins(数据存储值优先于客户端提交的值),此方法确保没有在没有通知用户正在发生的更改的情况下覆盖任何更改。

Detecting Concurrency Conflicts 检测并发冲突

要想通过解决EF抛出的OptimisticConcurrencyException来处理并发冲突,必须先知道什么时候会抛出这个异常,EF必须能够检测到冲突。因此必须对数据模型进行适当的调整。

有两种选择:

1、在数据库表中增加一列用来记录什么时候这行记录被更新的,然后就可以配置EF的Update或者Delete命令中的Where部分把这列加上;

一般这个跟踪记录列的类型为 rowversion ,一般是一个连续增长的值。在Update或者Delete命令中的Where部分包含上该列的原本值;

如果原有记录被其他人更新,那么这个值就会变化,那么Update或者Delete命令就会找不到原本数据行;这个时候,EF就会认为出现了并发冲突。

2、通过配置EF,在所有的Update或者Delete命令中的Where部分把所有数据列都包含上;和第1种方式一样,如果其中有一列数据被其他人改变了,那么Update或者Delete命令就不会找到原本数据行,这个时候,EF就会认为出现了并发冲突。

这个方式唯一问题就是where后面要拖很长很长的尾巴,而且以前版本中,如果where后面太长会引发性能问题,所以这个方式不被推荐,后面也不会再讲。

如果确定要采用这个方案,则必须为每一个非主键的Properites都加上ConcurrencyCheck属性定义,这个会让EF的update的WHERE加上所有的列;

Add an Optimistic Concurrency Property to the Department Entity

给Modles/Department 加上一个跟踪属性:RowVersion

public class Department
{
public int DepartmentID { get; set; } [StringLength(, MinimumLength = )]
public string Name { get; set; } [DataType(DataType.Currency)]
[Column(TypeName = "money")]
public decimal Budget { get; set; } [DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Start Date")]
public DateTime StartDate { get; set; } [Display(Name = "Administrator")]
public int? InstructorID { get; set; } [Timestamp]
public byte[] RowVersion { get; set; } public virtual Instructor Administrator { get; set; }
public virtual ICollection<Course> Courses { get; set; }
}

Timestamp 时间戳属性定义表示在Update或者Delete的时候一定要加在Where语句里;

叫做Timestamp的原因是SQL Server以前的版本使用timestamp 数据类型,后来用SQL rowversion取代了 timestamp 。

在.NET里 rowversion 类型为byte数组。

当然,如果喜欢用fluent API,你可以用IsConcurrencyToken方法来定义一个跟踪列:

modelBuilder.Entity<Department>()
.Property(p => p.RowVersion).IsConcurrencyToken();

记得变更属性后,要更新数据库,在PMC中进行数据库更新:

Add-Migration RowVersion
Update-Database

修改Department 控制器

先增加一个声明:

using System.Data.Entity.Infrastructure;

然后把控制器里4个事件里的SelectList里的 LastName 改为 FullName ,这样下拉选择框里就看到的是全名;显示全名比仅仅显示Last Name要友好一些。

下面就是对Edit做大的调整:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Edit(int? id, byte[] rowVersion)
{
string[] fieldsToBind = new string[] { "Name", "Budget", "StartDate", "InstructorID", "RowVersion" }; if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
} var departmentToUpdate = await db.Departments.FindAsync(id);
if (departmentToUpdate == null)
{
Department deletedDepartment = new Department();
TryUpdateModel(deletedDepartment, fieldsToBind);
ModelState.AddModelError(string.Empty,
"Unable to save changes. The department was deleted by another user.");
ViewBag.InstructorID = new SelectList(db.Instructors, "ID", "FullName", deletedDepartment.InstructorID);
return View(deletedDepartment);
} if (TryUpdateModel(departmentToUpdate, fieldsToBind))
{
try
{
db.Entry(departmentToUpdate).OriginalValues["RowVersion"] = rowVersion;
await db.SaveChangesAsync(); return RedirectToAction("Index");
}
catch (DbUpdateConcurrencyException ex)
{
var entry = ex.Entries.Single();
var clientValues = (Department)entry.Entity;
var databaseEntry = entry.GetDatabaseValues();
if (databaseEntry == null)
{
ModelState.AddModelError(string.Empty,
"Unable to save changes. The department was deleted by another user.");
}
else
{
var databaseValues = (Department)databaseEntry.ToObject(); if (databaseValues.Name != clientValues.Name)
ModelState.AddModelError("Name", "Current value: "
+ databaseValues.Name);
if (databaseValues.Budget != clientValues.Budget)
ModelState.AddModelError("Budget", "Current value: "
+ String.Format("{0:c}", databaseValues.Budget));
if (databaseValues.StartDate != clientValues.StartDate)
ModelState.AddModelError("StartDate", "Current value: "
+ String.Format("{0:d}", databaseValues.StartDate));
if (databaseValues.InstructorID != clientValues.InstructorID)
ModelState.AddModelError("InstructorID", "Current value: "
+ db.Instructors.Find(databaseValues.InstructorID).FullName);
ModelState.AddModelError(string.Empty, "The record you attempted to edit "
+ "was modified by another user after you got the original value. The "
+ "edit operation was canceled and the current values in the database "
+ "have been displayed. If you still want to edit this record, click "
+ "the Save button again. Otherwise click the Back to List hyperlink.");
departmentToUpdate.RowVersion = databaseValues.RowVersion;
}
}
catch (RetryLimitExceededException /* dex */)
{
//Log the error (uncomment dex variable name and add a line here to write a log.
ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
}
}
ViewBag.InstructorID = new SelectList(db.Instructors, "ID", "FullName", departmentToUpdate.InstructorID);
return View(departmentToUpdate);
}

可以看到,修改主要分为以下几个部分:
1、先通过ID查询一下数据库,如果不存在了,则直接提示错误,已经被其他用户删除了;

2、通过 db.Entry(departmentToUpdate).OriginalValues["RowVersion"] = rowVersion; 这个语句把原版本号给赋值进来;

3、EF在执行SaveChange的时候自动生成的Update语句会在where后面加上版本号的部分,如果语句执行结果没有影响到任何数据行,则说明出现了并发冲突;EF会自动抛出DbUpdateConcurrencyException异常,在这个异常里进行处理显示已被更新过的数据,比如告知用户那个属性字段被其他用户变更了,变更后的值是多少;

    var clientValues = (Department)entry.Entity;    //取的是客户端传进来的值
            var databaseEntry = entry.GetDatabaseValues();  //取的是数据库里现有的值 ,如果取来又是null,则表示已被其他用户删除

这里有人会觉得,不是已经在前面处理过被删除的情况,这里又加上出现null的情况处理,是不是多余,应该是考虑其他异步操作的问题,就是在第1次异步查询到最后SaveChange之间也可能被删除。。。(个人觉得第1次异步查询有点多余。。也许是为了性能考虑吧)

最后就是写一堆提示信息给用户,告诉用户哪个值已经给其他用户更新了,是否还继续确认本次操作等等。

对于Edit的视图也需要更新一下,加上版本号这个隐藏字段:

@model ContosoUniversity.Models.Department

@{
ViewBag.Title = "Edit";
} <h2>Edit</h2> @using (Html.BeginForm())
{
@Html.AntiForgeryToken() <div class="form-horizontal">
<h4>Department</h4>
<hr />
@Html.ValidationSummary(true)
@Html.HiddenFor(model => model.DepartmentID)
@Html.HiddenFor(model => model.RowVersion)

最后测试一下效果:

打开2个网页,同时编辑一个Department:

第一个网页先改预算为 0 ,然后点保存;

第2个网页改日期为新的日期,然后点保存,就出现以下情况:

这个时候如果继续点Save ,则会用最后一次数据更新到数据库:

忽然又有个想法,如果在第2次点Save之前,又有人更新了这个数据呢?会怎么样?

打开2个网页,分别都编辑一个Department ;

然后第1个网页把预算变更为 0 ;点保存;

第2个网页把时间调整下,点保存,这时候提示错误,不点Save ;

在第1个网页里,再编辑该Department ,把预算变更为 1 ,点保存;

回到第2个网页,点Save , 这时 EF会自动再次提示错误

下面对Delete 处理进行调整,要求一样,就是删除的时候要检查是不是原数据,有没有被其他用户变更过,如果变更过,则提示用户,并等待用户是否确认继续删除;

把Delete Get请求修改一下,适应两种情况,一种就是有错误的情况:

public async Task<ActionResult> Delete(int? id, bool? concurrencyError)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Department department = await db.Departments.FindAsync(id);
if (department == null)
{
if (concurrencyError.GetValueOrDefault())
{
return RedirectToAction("Index");
}
return HttpNotFound();
} if (concurrencyError.GetValueOrDefault())
{
ViewBag.ConcurrencyErrorMessage = "The record you attempted to delete "
+ "was modified by another user after you got the original values. "
+ "The delete operation was canceled and the current values in the "
+ "database have been displayed. If you still want to delete this "
+ "record, click the Delete button again. Otherwise "
+ "click the Back to List hyperlink.";
} return View(department);
}

把Delete Post请求修改下,在删除过程中,处理并发冲突异常:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Delete(Department department)
{
try
{
db.Entry(department).State = EntityState.Deleted;
await db.SaveChangesAsync();
return RedirectToAction("Index");
}
catch (DbUpdateConcurrencyException)
{
return RedirectToAction("Delete", new { concurrencyError = true, id=department.DepartmentID });
}
catch (DataException /* dex */)
{
//Log the error (uncomment dex variable name after DataException and add a line here to write a log.
ModelState.AddModelError(string.Empty, "Unable to delete. Try again, and if the problem persists contact your system administrator.");
return View(department);
}
}

最后要修改下Delete的视图,把错误信息显示给用户,并且在视图里加上DepartmentID和当前数据版本号的隐藏字段:

@model ContosoUniversity.Models.Department

@{
ViewBag.Title = "Delete";
} <h2>Delete</h2> <p class="error">@ViewBag.ConcurrencyErrorMessage</p> <h3>Are you sure you want to delete this?</h3>
<div>
<h4>Department</h4>
<hr />
<dl class="dl-horizontal">
<dt>
Administrator
</dt> <dd>
@Html.DisplayFor(model => model.Administrator.FullName)
</dd> <dt>
@Html.DisplayNameFor(model => model.Name)
</dt> <dd>
@Html.DisplayFor(model => model.Name)
</dd> <dt>
@Html.DisplayNameFor(model => model.Budget)
</dt> <dd>
@Html.DisplayFor(model => model.Budget)
</dd> <dt>
@Html.DisplayNameFor(model => model.StartDate)
</dt> <dd>
@Html.DisplayFor(model => model.StartDate)
</dd> </dl> @using (Html.BeginForm()) {
@Html.AntiForgeryToken()
@Html.HiddenFor(model => model.DepartmentID)
@Html.HiddenFor(model => model.RowVersion) <div class="form-actions no-color">
<input type="submit" value="Delete" class="btn btn-default" /> |
@Html.ActionLink("Back to List", "Index")
</div>
}
</div>

最后看看效果:

打开2个网页进入Department Index页面,第1个页面点击一个Department的Edit ,第2个页面点击该 Department的Delete;

然后第一个页面把预算改为100,点击Save.

第2个页面点击Delete 确认删除,会提示错误:

EF学习笔记(十) 处理并发的更多相关文章

  1. EF学习笔记(十二):EF高级应用场景

    学习总目录:ASP.NET MVC5 及 EF6 学习笔记 - (目录整理) 上篇链接:EF学习笔记(十一):实施继承 本篇原文链接:Advanced Entity Framework Scenari ...

  2. EF学习笔记(十一):实施继承

    学习总目录:ASP.NET MVC5 及 EF6 学习笔记 - (目录整理) 上篇链接:EF学习笔记(十) 处理并发 本篇原文链接:Implementing Inheritance 面向对象的世界里, ...

  3. python3.4学习笔记(十四) 网络爬虫实例代码,抓取新浪爱彩双色球开奖数据实例

    python3.4学习笔记(十四) 网络爬虫实例代码,抓取新浪爱彩双色球开奖数据实例 新浪爱彩双色球开奖数据URL:http://zst.aicai.com/ssq/openInfo/ 最终输出结果格 ...

  4. Learning ROS for Robotics Programming Second Edition学习笔记(十) indigo Gazebo rviz slam navigation

    中文译著已经出版,详情请参考:http://blog.csdn.net/ZhangRelay/article/category/6506865 moveit是书的最后一章,由于对机械臂完全不知,看不懂 ...

  5. EF学习笔记(九):异步处理和存储过程

    总目录:ASP.NET MVC5 及 EF6 学习笔记 - (目录整理) 上一篇:EF学习笔记(八):更新关联数据 本篇原文:Async and Stored Procedures 为何要采用异步? ...

  6. EF学习笔记(八):更新关联数据

    学习笔记主目录链接:ASP.NET MVC5 及 EF6 学习笔记 - (目录整理) 上一篇链接:EF学习笔记(七):读取关联数据 本篇原文链接:Updating Related Data 本篇主要考 ...

  7. python3.4学习笔记(十八) pycharm 安装使用、注册码、显示行号和字体大小等常用设置

    python3.4学习笔记(十八) pycharm 安装使用.注册码.显示行号和字体大小等常用设置Download JetBrains Python IDE :: PyCharmhttp://www. ...

  8. python3.4学习笔记(十九) 同一台机器同时安装 python2.7 和 python3.4的解决方法

    python3.4学习笔记(十九) 同一台机器同时安装 python2.7 和 python3.4的解决方法 同一台机器同时安装 python2.7 和 python3.4不会冲突.安装在不同目录,然 ...

  9. python3.4学习笔记(十六) windows下面安装easy_install和pip教程

    python3.4学习笔记(十六) windows下面安装easy_install和pip教程 easy_install和pip都是用来下载安装Python一个公共资源库PyPI的相关资源包的 首先安 ...

随机推荐

  1. pandas进行条件格式化以及线性回归的预测

    条件格式化 需求1: 将三次考试的成绩小于60分的值找出来,并将字体变为红色 需求2: 将每次考试的第一名找出来,将背景变为绿色 需求3: 使用背景颜色的深浅来表示数值的大小 需求4: 使用数据条的长 ...

  2. 基于上三角变换或基于DFS的行(列)展开的n阶行列式求值算法分析及性能评估

    进入大一新学期,看完<线性代数>前几节后,笔者有了用计算机实现行列式运算的想法.这样做的目的,一是巩固自己对相关概念的理解,二是通过独立设计算法练手,三是希望通过图表直观地展现涉及的两种算 ...

  3. CodeForces-4C Registration system

    // Registration system.cpp : 此文件包含 "main" 函数.程序执行将在此处开始并结束. // #include <iostream> # ...

  4. HBase Master HA高可用

    HMaster没有单点问题,HBase中可以启动多个HMaster,通过Zookeeper的Master Election机制保证总有一个Master运行. 所以这里要配置HBase高可用的话,只需要 ...

  5. python——获取文件列表

    """-------------------------------------------------------- <<获取文件列表>> () ...

  6. jasper打印文件出现空白页面

    EG:打印文件结果打印出一片空白 原因:使用了null的数据源而不是JREmptyDataSource 以下为正确代码 public <T> List<JasperPrint> ...

  7. layabox typescript 安装固定版本

    安装最新版本方法: https://blog.csdn.net/adelais__/article/details/79181474 固定版本(比如2.1.5): C:\Users\Administr ...

  8. OSPFv3综合实验(GNS3)

    一.实验目的 1.  掌握 OSPFv3(v2) 的配置方法 2.  掌握在帧中继环境下 OSPFv3 (v2)的配置方法 3.  掌握 OSPFv3(v2) NSSA 的配置方法 4.  掌握外部路 ...

  9. Pytorch之训练器设置

    Pytorch之训练器设置 引言 深度学习训练的时候有很多技巧, 但是实际用起来效果如何, 还是得亲自尝试. 这里记录了一些个人尝试不同技巧的代码. tensorboardX 说起tensorflow ...

  10. java GUI的效果图

    import java.awt.*;import javax.swing.*; public class GridBagDemo extends JFrame {    public static v ...