在项目开发中,我们有时需要对数据并发请求进行处理。举个简单的例子,比如接单系统中,AB两个客服同时请求处理同一单时,应该只有一单请求是处理成功的,另外一单应当提示客服,此单已经被处理了,不需要再处理。如果我们不对上述并发冲突进行检测处理,两个请求都会成功,数据库接收到的后面的请求将覆盖前面的请求。在大部分应用程序中,这种方式是可以接受的,但是举例的接单系统中这种处理方式显然不符合业务要求。

一、认识并发

数据库操作有ACID属性(原子性,一致性,隔离性和永久性),并发问题即在相同的数据上同时执行多个数据库操作,其中更新操作可能导致数据的不一致性,使程序的业务数据发生错误。

想象一下下面几种并发场景:

  1. 事务A读取某实体数据后提交修改,提交之前事务B读取了该实体数据;
  2. 事务A读取某实体数据后,事务B更新或删除了该实体数据,事务A再次读取或修改该实体数据;
  3. 事务A读取并修改实体所有数据,提交之前事务B插入了一条新的实体数据。

结果就是场景1发生了脏读,场景2发生了不可重复读,场景3发生了幻读。这在一些应用程序中是不能接受的错误。

这里很容易使我们联想到了数据库事务的隔离级别:

  1. Serializable:可避免脏读、不可重复读、幻读的发生;
  2. Repeatable read:可避免脏读、不可重复读的发生;
  3. Read committed:可避免脏读的发生(EF的默认隔离级别);
  4. Read uncommitted:最低级别,任何情况都无法保证。

而不同的隔离级别就是通过不同的级别或类型锁来实现的。

所以大家都知道了可以利用事务隔离级别或数据库锁来解决并发问题,目标是永远不让任何冲突发生,这个处理方式被称之为悲观并发

当从数据库读取数据时,需要先请求一个只读锁(即共享锁)或更新锁(即独占锁),如果你申请了只读锁,其他人仍然可以申请只读锁,如果你申请了更新锁,则其他人都不能再申请锁。所以更新锁只能在数据无任何锁的情况下申请。

设想下面的情况,事务T1,T2并发处理时,T1申请了TableA的只读锁,又申请TableB的更新锁,而恰巧T2已经申请了TableB的只读锁,又需要申请TableA的更新锁,此时就会出现死锁,卡在这里直到有一方先取消锁。

管理锁会让编程变得复杂,应用程序不得不管理每个操作正在使用的所有锁,而且它需要消耗大量的数据库管理资源,明显的降低系统的并发性和执行效率。EF也并不直接支持悲观并发,所以我们尽可能不要使用悲观并发。

悲观并发的替代方案就是乐观并发。乐观并发中,数据库级别不放置任何显式锁,数据库操作会按照接收到的命令顺序执行。无论应用程序何时从数据库请求数据,数据都会被读取并保存到应用程序内存中。此时并发冲突是由应用程序进行处理的,在应用程序中根据项目实际需求设定冲突处理策略,如让用户知道他提交的修改因为并发冲突没有成功。乐观并发的本质是允许冲突发生,然后通过冲突处理策略来解决冲突。

比如以下的冲突处理策略:

  1. 忽略冲突,保留最后一次数据修改的值;
  2. 忽略修改,保留数据库中的值;
  3. 合并修改值,针对同实体的不同列属性同时更新;
  4. 交由用户选择。

策略1的结果,会导致潜在的数据丢失,并发时只有最后一个用户的更改可见,其余丢失。策略2的结果则是更新失败,应提示用户由于数据已经被他人更新导致此次更新失败。策略3的结果,更新不同的列均会成功。策略4的结果,提示用户该数据已经被他人更改了,询问他是否仍然要提交更新还是改为查看已经更新的数据。

二、设计处理乐观并发的应用

事实上,不借助EF并发处理技术单纯依靠应用程序来实现乐观并发的处理是很复杂的,但是仅仅处理字段级别并发的应用,也就是针对同实体的不同属性列并发更新,还是比较容易实现的。

比如TableA有ABC三列,应用中T1,T2同时读取了某一条记录,然后T1修改了此条记录A,B列并提交,随后T2修改了此记录C列并提交,最终结果是T2的修改覆盖了T1的修改,T1的修改丢失。

针对这类并发,我们可以这样设计。由于EF的更新是全字段更新,所以我们应首先对比原实体和更改后的实体之间的列差异,然后读取最新的实体,更改这些差异列,提交修改。这样的结果每次都是只更新需要更新的列,达到了避免全字段更新的目的,最终保证了多个用户并发更新同记录不同字段,所有更改都会有效持久化至数据库。

具体实现方式例如,T1初次读到的记录为originalEntity,修改后的记录为targetEntity,提交更改时,读取数据库中当前最新的记录为currentEntity,然后对比originalEntity和targetEntity的差异列,并将差异列赋值给currentEntity,最终提交更新currentEntity即可。

三、EF的乐观并发应用

EF处理并发,首先实体框架必须能够检测冲突,那么就需要一个列来跟踪修改,在更新或删除的时候包含该列,与数据库中的值进行对比,如果不一致就说明发生了冲突。EF中使用的是行版本RowVersion,其值在每次更新时都会自动更新。

具体实现也很简单,我们只需要在实体中增加一个byte[]类型的字段,并为其加上[Timestamp]注解,如下图示:

这样在更新或删除操作生成的Sql中,Where语句将包含 and v = @2 条件,如果有其它用户更改过此行,那么行版本将不一致,因此更新或删除sql会无法找到要更新的行,此时EF将认定该操作出现了并发冲突。

然后应用程序收到异常消息,异常类型如下:

  1. EF 自定义异常:System.Data.Entity.Infrastructure.DbUpdateConcurrencyException
  2. net FrameWorkn异常:System.Data.OptimisticConcurrencyException
System.Data.Entity.Infrastructure.DbUpdateConcurrencyException: Store update, insert, or delete statement affected an unexpected number of rows (0). Entities may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=472540 for information on understanding and handling optimistic concurrency exceptions. ---> System.Data.Entity.Core.OptimisticConcurrencyException: Store update, insert, or delete statement affected an unexpected number of rows (0). Entities may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=472540 for information on understanding and handling optimistic concurrency exceptions.
在 System.Data.Entity.Core.Mapping.Update.Internal.UpdateTranslator.ValidateRowsAffected(Int64 rowsAffected, UpdateCommand source)
在 System.Data.Entity.Core.Mapping.Update.Internal.UpdateTranslator.<UpdateAsync>d__0.MoveNext()
--- 引发异常的上一位置中堆栈跟踪的末尾 ---
在 System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
在 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
在 System.Data.Entity.Core.Objects.ObjectContext.<ExecuteInTransactionAsync>d__3d`1.MoveNext()
--- 引发异常的上一位置中堆栈跟踪的末尾 ---
在 System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
在 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
在 System.Data.Entity.Core.Objects.ObjectContext.<SaveChangesToStoreAsync>d__39.MoveNext()
--- 引发异常的上一位置中堆栈跟踪的末尾 ---
在 System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
在 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
在 System.Data.Entity.SqlServer.DefaultSqlExecutionStrategy.<ExecuteAsyncImplementation>d__9`1.MoveNext()
--- 引发异常的上一位置中堆栈跟踪的末尾 ---
在 System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
在 System.Runtime.CompilerSer...

然后应用程序就可以根据项目实际需求,制定冲突处理策略,来合理处理并发冲突了。

示例代码如下:

 try
{
//保存时TimeStamp值如果不匹配,就认定出现并发冲突,于是抛出异常
context.SaveChanges();
}
//catch (DbUpdateConcurrencyException ex) //EF自定义异常
//{
//}
catch (System.Data.OptimisticConcurrencyException ex) //.net FreamWork定义异常
{
//RefreshMode.ClientWins,捕获异常后依然对数据进行保存
context.Refresh(RefreshMode.ClientWins, myEntity);
//RefreshMode.StoreWins,保存数据库中原有值,或合并不同列修改值
context.Refresh(RefreshMode.StoreWins, myEntity);
context.SaveChanges(); }
catch (System.Data.OptimisticConcurrencyException ex)
{
//捕获异常后不做处理,将消息返回客户端
string message = "数据已被他人修改,请刷新";
return message;
}

示例代码中可以看到处理策略类型使用的几种模式:

  1. 保留最后一次提交修改的值,用RefreshMode.ClientWins
  2. 保留数据库中的值,用 RefreshMode.StoreWins
  3. 合并修改值,针对同实体不同列也是用RefreshMode.StoreWins
 namespace System.Data.Entity.Core.Objects
{
//
// 摘要:
// Defines the different ways to handle modified properties when refreshing in-memory
// data from the database.
[SuppressMessage("Microsoft.Design", "CA1008:EnumsShouldHaveZeroValue")]
public enum RefreshMode
{
//
// 摘要:
// Discard all changes on the client and refresh values with store values. Client
// original values is updated to match the store.
StoreWins = ,
//
// 摘要:
// For unmodified client objects, same behavior as StoreWins. For modified client
// objects, Refresh original values with store value, keeping all values on client
// object. The next time an update happens, all the client change units will be
// considered modified and require updating.
ClientWins =
}
}

以上我们通过RowVersion实现了数据行级并发控制,有时我们的目标是只需要控制某个列的并发,那么我们应使用[ConcurrencyCheck]注解,将ConcurrencyCheck特性添加到实体需要控制并发的非主键属性上,即可实现列级并发控制。

--End--

EF|CodeFirst数据并发管理的更多相关文章

  1. EF Code-First数据迁移

    Code-First数据迁移  首先要通过NuGet将EF升级至最新版本. 新建MVC 4项目MvcMigrationDemo 添加数据模型 Person 和 Department,定义如下: usi ...

  2. 【EF】EF Code-First数据迁移

    Code-First数据迁移  首先要通过NuGet将EF升级至最新版本. 新建MVC 4项目MvcMigrationDemo 添加数据模型 Person 和 Department,定义如下: usi ...

  3. EF Code-First数据迁移的尝试

    Code-First的方式虽然省去了大量的sql代码,但增加了迁移的操作.尝试如下: 1.首先要在“扩展管理器”里搜索并安装NuGet“库程序包管理器”,否则所有命令都不能识别,会报CommandNo ...

  4. EF CodeFirst数据注解特性详解

    数据注解特性是.NET特性,可以在EF或者EF Core中,应用于实体类上或者属性上,以重写默认的约定规则. 在EF 6和EF Core中,数据注解特性包含在System.ComponentModel ...

  5. EF CodeFirst数据迁移与防数据库删除

    1 开启migrations功能 enable-migrations -force 2 添加迁移版本 add-migration 名称后缀 我们每次修改实体后,都应该使用这个add-migration ...

  6. EF Codefirst 初步学习(二)—— 程序管理命令 更新数据库

    前提:搭建成功codefirst相关代码,参见EF Codefirst  初步学习(一)--设置codefirst开发模式 具体需要注意点如下: 1.确保实体类库程序生成成功 2.确保实体表类库不缺少 ...

  7. 1.【使用EF Code-First方式和Fluent API来探讨EF中的关系】

    原文链接:http://www.c-sharpcorner.com/UploadFile/3d39b4/relationship-in-entity-framework-using-code-firs ...

  8. 新年奉献MVC+EF(CodeFirst)+Easyui医药MIS系统

    本人闲来无事就把以前用Asp.net做过的一个医药管理信息系统用mvc,ef ,easyui重新做了一下,业务逻辑简化了许多,旨在加深对mvc,ef(codefirst),easyui,AutoMap ...

  9. Entity Framework CodeFirst数据迁移

    前言 紧接着前面一篇博文Entity Framework CodeFirst尝试. 我们知道无论是“Database First”还是“Model First”当模型发生改变了都可以通过Visual ...

随机推荐

  1. VO、DTO、DO、PO

    领域模型中的实体类可细分为4种类型:VO.DTO.DO.PO. PO(Persistent Object):持久化对象,表示持久层的数据结构(如数据库表): DO(Domain Object):领域对 ...

  2. Linux-系统调用理解

    系统调用即为Linux内核中设置的一组用于实现各种系统功能的子程序,操作系统通过系统调用为运行在其上的进程提供服务. 由于进程一般不能访问内核所占内存空间以及调用内核函数,为了与用户态进程进行交互,内 ...

  3. python_类与对象学习笔记

    class Phone: #手机属性===>类属性 # color='black' # price=4500 # brand='oppo' # size='5.5' #参数化-魔法方法--初始化 ...

  4. LA4255/UVa1423 Guess 拓扑排序 并查集

    评分稍微有一点过分..不过这个题目确确实实很厉害,对思维训练也非常有帮助. 按照套路,我们把矩阵中的子段和化为前缀和相减的形式.题目就变成了给定一些前缀和之间的大小关系,让你构造一组可行的数据.这个东 ...

  5. python3 两层dict字典转置

    python3; 两层字典 dict =(type, dict2) dict2 = (k_value, index) dictss = { 10: {3: 1, 4: 2, 5: 3, 6: 4, 7 ...

  6. 重装了Devexpress后项目报Dll引用找不到问题解决办法

    最近将我的开发环境从VS2015升级到VS2017,升级完后报如下错误,找不到Dev的引用,明明是重新装了dev为什么找不到呢? 经过查看dll引用地址,发现我的dev一开始是安装在C盘,dll引用路 ...

  7. 责任链模式-Chain of Responsibility(Java实现), 例1

    责任链模式-Chain of Responsibility, 例1 在这种模式中,通常每个接收者都包含对另一个接收者的引用.如果一个对象不能处理该请求,那么它会把相同的请求传给下一个接收者,依此类推. ...

  8. python之路(11)描述符

    前言 描述符是用于代理另一个类的属性,一般用于大型的框架中,在实际的开发项目中较少使用,本质是一个实现了__get__(),__set__(),__delete__()其中一个方法的新式类 __get ...

  9. 自搭的一个系统框架,使用Spring boot+Vue+Element

    基于:jdk1.8.spring boot2.1.3.vue-cli3.4.1 特性:    ~ 数据库访问使用spring data jpa+alibaba druid    ~ 前后端数据交互使用 ...

  10. H5_0005:JS判断域名和时间有效期的方法

    (function () { var n = { c: function (t, e) { //console.log("c"); //把i(15)的d数组转换成字串 for (v ...