EF|CodeFirst数据并发管理

在项目开发中,我们有时需要对数据并发请求进行处理。举个简单的例子,比如接单系统中,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...

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

示例代码如下:

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

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

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

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

 

posted @ 2018-06-09 20:49  览岳  阅读(1115)  评论(0编辑  收藏  举报