一、背景
我们知道,为了防止并发而出现脏读脏写的情况,可以使用Lock语句关键字,这属于悲观并发控制的一种技术,,但在分布式站点下,锁的作用几乎不存在,因为虽然锁住了A服务器的实例对象,但B服务器上的锁是不知道的A服务器上锁的情况的,所以,面对分布式站点、单一数据库这种架构,我们可以使用EntityFramework的乐观并发控制来解决这个问题,EF对并发控制有不管控和乐观并发控制两种,默认情况是不管控,但EF不支持悲观并发。
lock 确保当一个线程位于代码的临界区时,另一个线程不进入临界区。如果其他线程试图进入锁定的代码,则它将一直等待(即被阻止),直到该对象被释放。
二、悲观并发和乐观并发
悲观并发:比如有两个用户A,B,同时登录系统修改一个文档,如果A先进入修改,则系统会把该文档锁住,B就没办法打开了,只有等A修改完,完全退出的时候B才能进入修改。
乐观并发:同上面的例子,A,B两个用户同时登录,如果A先进入修改紧跟着B也进入了。A修改文档的同时B也在修改。如果在A保存之后B再保存他的修改,此时系统检测到数据库中文档记录与B刚进入时不一致,B保存时会抛出异常,修改失败。
乐观并发的基本出发点是:当保存数据的时候抱着一种乐观的态度,不期望发生并发冲突,即使万一发生并发冲突,也能捕捉到冲突异常,然后根据策略解决冲突,而解决冲突的方式一般分为Client wins(以后操作者为赢) 和 Store wins(以先存储的数据为赢)。
三、EF中如何控制并发
第1步、
在设计器中,对需要进行并发控制的字段的ConcurrencyMode并发模式设置为Fixed,这是检测是否发生冲突的指标,该字段最终会在EF生成SQL时的where子句出现,如果没有设置为Fixed,即使该字段出现并发冲突,EF也不会报出并发异常,从而会导致出现脏读脏写的情况。
第2步、
Resolving optimistic concurrency exceptions with Reload
使用Reload数据作为解决乐观并发异常的策略之一,我在这里就讲数据Reload这一种策略就好了,除了Reload外,还有其他几种冲突解决策略,详见参考文献中EF官方团队博客。
微软Entity Framework 团队官方博客 推荐处理乐观并发冲突的策略之一是Reload数据,也就是EF检测到并发冲突时会抛出DbUpdateConcurrencyException,这时解决冲突分为Client Wins或者Store Wins ,而Reload处理也就是Store Wins,意味着放弃当前内存中的实体,重新到数据库中加载当前实体,EF官方团队给出来的示例代码如下,其他几种策略请见参考文献链接。
using (var context = new UnicornsContext()) { bool saveFailed; do { saveFailed = false; var unicorn = context.Unicorns.Find(1); unicorn.Name = "tom"; try { context.SaveChanges(); } catch (DbUpdateConcurrencyException ex) { saveFailed = true; // Update the values of the entity that failed to save // from the store ex.Entries.Single().Reload(); } } while (saveFailed); }
四、SqlProfiler看原理
完成以上步骤后,您的程序就具备了乐观并发处理的能力了,但是,其中的原理是什么呢?从SqlProfiler监控结果来看,EF是将并发模式设置为Fixed的字段放在where子句里,后操作会因为取不到相应的记录而更新失败,打个比方说,当两个用户同时对同一条记录(ID=1,Count=10)进行读、写操作的时候,A、B同时读取记录的时候,Count都等于10,A用户先进行Update减1操作,此时(ID=1,Count=9),而此时B用户稍微晚一点点再进行Update操作的时候,因为Count已经被A修改成9了,已经不存在(ID=1,Count=10)的记录了,所以B最终执行影响行数为0,EntityFramework抛出并发异常:
如:我们给Count属性的并发模式设置成Fixed的话,那生成的SQL语句如下:
exec sp_executesql N‘update [dbo].[OrderLog] set [Count] = @0 where (([ID] = @1) and ([Count] = @2))‘,N‘@0 int,@1 int,@2 int‘,@0=78,@1=1,@2=9
当EntityFramework执行更新操如果影响行数为0,就会抛出异常,相关源代码如下所示:
五、总结
个人认为,乐观并发控制适用于并发量还不是很大情况,也就是符合乐观并发的初衷,当保存数据的时候不期望发生并发冲突,一旦发生冲突,也能捕捉到冲突异常,然后根据策略解决冲突或者提示用户操作失败等,但是,当并发量很大的时候,我认为乐观并发就显得并不那么适用了,个人建议,做评估项目并发风险和做并发冲突的测试,假如完全可以接受,大可以应用EntityFramework的乐观并发控制,实现起来也比较简单,假如项目并发量确实很大,那可以考虑别的技术方案实现,比如消息队列……等。
参考文献
(1)微软EntityFramework团队博客: Using DbContext in EF 4.1 Part 9: Optimistic Concurrency Patterns
(2)Gyoung: Entity Framework 并发处理
Entity Framework 乐观并发控制