本篇原文链接: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 确认删除,会提示错误:

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

  1. 【EF6学习笔记】目录

    [EF6学习笔记](一)Code First 方式生成数据库及初始化数据库实际操作 [EF6学习笔记](二)操练 CRUD 增删改查 [EF6学习笔记](三)排序.过滤查询及分页 [EF6学习笔记]( ...

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

    个人从传统的CS应用开发(WPF)开始转向BS架构应用开发: 先是采用了最容易上手也是最容易搞不清楚状况的WebForm方式入手:到后面就直接抛弃了服务器控件的开发方式,转而采用 普通页面+Ajax+ ...

  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. EF6学习笔记(六) 创建复杂的数据模型

    EF6学习笔记总目录:ASP.NET MVC5 及 EF6 学习笔记 - (目录整理) 本篇原文地址:Creating a More Complex Data Model 本篇讲的比较碎,很多内容本人 ...

  6. EF6 学习笔记(五):数据库迁移及部署

    EF6学习笔记总目录:ASP.NET MVC5 及 EF6 学习笔记 - (目录整理) 原文地址:Code First Migrations and Deployment 原文主要讲两部分:开发环境下 ...

  7. EF6学习笔记(四) 弹性连接及命令拦截调试

    EF6学习笔记总目录:ASP.NET MVC5 及 EF6 学习笔记 - (目录整理) 本章原文地址:Connection Resiliency and Command Interception 原文 ...

  8. EF6 学习笔记(三):排序、过滤查询及分页

    EF6 学习笔记索引目录页: ASP.NET MVC5 及 EF6 学习笔记 - (目录整理) 上篇:EF6 学习笔记(二):操练 CRUD 增删改查 本篇原文地址:Sorting, Filterin ...

  9. EF6 学习笔记(二):操练 CRUD 增删改查

    EF6学习笔记总目录 ASP.NET MVC5 及 EF6 学习笔记 - (目录整理) 接上篇: EF6 学习笔记(一):Code First 方式生成数据库及初始化数据库实际操作 本篇原文链接: I ...

  10. EF6 学习笔记(一):Code First 方式生成数据库及初始化数据库实际操作

    EF6 学习笔记总目录:ASP.NET MVC5 及 EF6 学习笔记 - (目录整理) 本篇参考原文地址: Creating an Entity Framework Data Model 说明:学习 ...

随机推荐

  1. python 基础———— 字符串常用的调用 (图)

    Python 常用的 字符串调用方法 这里用到了pycharm ( 使用Python  有力的工具) 下载地址https://www.jetbrains.com/pycharm/download/#s ...

  2. 《Linux就该这么学》第十五天课程

    本次课所学习的是DNS域名解析服务! 下面提供一些DNS有关的内容 如需进一步学习,请前往https://www.linuxprobe.com/chapter-13.html 工作模式: 1.主服务器 ...

  3. sjms-3 结构型模式

    结构型模式 适配器模式 内容:将一个类的接口转换成客户希望的另一个接口.适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作.两种实现方式:类适配器:使用多继承对象适配器:使用组合 角色 ...

  4. PAT DFS,BFS,Dijkstra 题号

    为什么要分类刷题: 因为刷⼀道算法题需要花⼀两个⼩时甚⾄半天,平时我们还要上课做别的事情,你在⼀段时间内刷算法如果只按照顺序,可能今天遇到了⼀道最短路径的题⽬,弄了半天好不容易看懂了别⼈的代码,以为⾃ ...

  5. [Java基础复习] -- x. 正则表达式的使用

    序号待定, 先用x占位表示 理论知识待完善, 先贴上代码 import java.util.regex.Matcher; import java.util.regex.Pattern; import ...

  6. ASP.NET代码调用SQL Server带DateTime类型参数的存储过程抛出异常问题

    ASP.NET代码调用SQL Server带DateTime类型参数的存储过程,如果DateTime类型参数的值是'0001/1/1 0:00:00'时,就会抛出异常“Message: SqlDate ...

  7. Exp 8 Web基础

    Exp 8 Web基础 20154305 齐帅 一.实践要求: (1).Web前端HTML 能正常安装.启停Apache.理解HTML,理解表单,理解GET与POST方法,编写一个含有表单的HTML. ...

  8. iOS 数据归档----温故而知新

    #import "StudyViewController.h" #import "person.h" @interface StudyViewControlle ...

  9. 河北大学python选修课00次作业

    学习python认为挺好玩的一件事.看到很多关于python的东西在网上,看到有这个课,认为只是选修课,别人也可以选,自己想不能被别人落下,别人都会,我不会可不行. 而且认为python是一个很强大的 ...

  10. CLion之C++框架篇-优化框架,单元测试(二)

    背景   结合上一篇CLion之C++框架篇-安装工具,基础框架的搭建(一),继续进行框架优化!   googletest(GTest)是Google开源的C++测试框架,与CLion组合,对C++环 ...