EF6学习笔记二十七:并发冲突(一)
要专业系统地学习EF推荐《你必须掌握的Entity Framework 6.x与Core 2.0》。这本书作者(汪鹏,Jeffcky)的博客:https://www.cnblogs.com/CreateMyself/
来到并发这里了,我自己得先承认,并发对我来说完全是一个熟悉又真正陌生的东西,总的来说,我对并发一无所知。
那么不管是怎么回事,我也要说一下。之前看过零星的一些讲硬件的东西,说的是,很多个应用你看似同时开启,同时运行的,其实只是,CPU速度太快,让你察觉不了。
所以不可能存在两个任务同时进行,这只是错觉。所以我现在给自己一些自信,我断定!不存在,就像一个啤酒瓶,口就那么大,一次只容许一颗珠子进去,不可能两个同时进去,都是错觉!
来看EF中的并发。
我们在使用EF上下文时,遵循的是一个请求对应一个上下文,对事务也是这个态度,不要事务那么长,越短越好。
一个请求对应一个上下文,那么服务器同时接受到了多个请求,构造出多个上下文对象,针对同一资源操作,问题就出来这里。
因为不同的上下文中查询出的实体都是各自的,并不是同一个引用。
这里有两个上下文,都得到了名叫“张三”的学生实体,第一个上下文修改为“李四”,第二个上下文修改为“张三”,那么最终的结果应该是“张三”,但是看看下面的代码,其实最终数据库的结果是“李四”
using (DB1_Context ctx1 = new DB1_Context()) using (DB1_Context ctx2 = new DB1_Context()) { var stu1 = ctx1.Students.FirstOrDefault(); var stu2 = ctx2.Students.FirstOrDefault(); stu1.Name = "李四"; stu2.Name = "张三"; ctx1.SaveChanges(); ctx2.SaveChanges(); }
你觉得应该是第一个上文查询修改完,再第二个上下文接着查询修改就行了。但是高并发的情况下是无法保证的。
那么我们看下一个上下文中查询相同的两个实体。引用是相等的。所以整个解决方案就使用一个上下文是不是就行了?我觉得是,但是这是不科学的。
using (DB1_Context ctx = new DB1_Context()) { var stu1 = ctx.Students.FirstOrDefault(); var stu2 = ctx.Students.FirstOrDefault(); Console.WriteLine(ReferenceEquals(stu1, stu2)); // True Console.WriteLine($"stu1.Name:{stu1.Name},stu2.Name:{stu2.Name}"); // stu1.Name:小新77,stu2.Name:小新77 }
并发冲突做个分初级、中级和高级来讲,我这篇笔记主要记录初级内容的学习心得。
现在来认识一下悲观并发和乐观并发,这是两种并发的控制方法
悲观并发:当更新特定记录时,同一记录上的所有其他并发更新将被阻塞,直到当前操作完成或者放弃,其他并发操作才可以继续。
乐观并发:当更新特定记录时,同一记录上的所有其他并发将导致最后一条记录被保存(获胜)。假设由于并发访问共享资源而导致资源冲突并不是不可能过的,而是不可用的,此时将采取一定手段来解决并发冲突。
上面的张三李四就是属于乐观并发,就是我就随他去了,它自己修改到哪里就是哪里,我也不关心过程。
那么如何解决上面的问题,上面是什么问题?就是我第二个上下文查询出实体不是最新的,应该将这种情况看做是一种异常,但是如果你用Try/catch来捕获是捕获不到的。
因为捕获并发冲突需要特殊配置,EF就为我们提供了两种方式:并发Token、行版本(RowVersion)
如果我们对student的Name属性这是并发Token,需要将属性进行如下配置
modelBuilder.Entity<Student>().Property(x => x.Name).IsConcurrencyToken();
现在来用try/catch就可以捕获了
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.
这个异常我之前没有学到这里来的时候碰到过,没有记录下来当时是写的什么代码,真可惜!
来看看行版本的方式。这就需要为实体添加一个字节数组类型的属性,并且该属性需要配置
public byte[] RowVersion { get; set; }
modelBuilder.Entity<Student>().Property(x => x.RowVersion).IsRowVersion();
在数据库中就是这样的,每次数据更新时,数据库中的RowVersion也会如时间戳一样得到更新,从而检测数据库中所存储的值与实体中的值是否一致来检测并发冲突。
那么接下来我们就开始在异常处理中进行操作,他不是数据不是最新的吗?那么我就让他得到最新的。因为EF中有针对并发异常的类(DbUpdateConcurrencyException)。
DbUpdateConcurrencyException中具有Entries属性,该属性返回一系列DbEntityEntry对象,表示冲突实体的跟踪信息。
using (DB1_Context ctx1 = new DB1_Context()) using (DB1_Context ctx2 = new DB1_Context()) { var stu1 = ctx1.Students.FirstOrDefault(); var stu2 = ctx2.Students.FirstOrDefault(); stu1.Name = "小新111"; ctx1.SaveChanges(); stu2.Name = "小新222"; try { ctx2.SaveChanges(); } catch (DbUpdateConcurrencyException ex) { var s = ex.Entries.Single(); s.Reload(); Console.WriteLine("stu2.Name:" + stu2.Name); // 小新11 stu2.Name = "小新222"; ctx2.SaveChanges(); throw ex; } }
调用Reload方法来刷新数据库中的最新值到当前内存中的值,就是造成并发冲突的这个对象,更新它。
如果说不用Relod,也有另外一种方式来实现
using (DB1_Context ctx1 = new DB1_Context()) using (DB1_Context ctx2 = new DB1_Context()) { //ctx1.Database.Log = msg => Console.WriteLine("ctx11111111111111:" + msg); //ctx2.Database.Log = msg => Console.WriteLine("ctx22222222222222:" + msg); var stu1 = ctx1.Students.FirstOrDefault(); var stu2 = ctx2.Students.FirstOrDefault(); stu1.Name = "小新11"; ctx1.SaveChanges(); stu2.Name = "小新22"; try { ctx2.SaveChanges(); } catch (DbUpdateConcurrencyException ex) { // 获取并发异常被追踪的实体 var tracking = ex.Entries.Single(); // 获取数据库原始值对象,数据库中没被修改之前的值 var original = (Student)tracking.OriginalValues.ToObject(); Console.WriteLine(original.Name); // 小新 // 获取更新后数据库最新的值对象,就是数据库中目前的值,这一句会发起查询 var database = (Student)tracking.GetDatabaseValues().ToObject(); Console.WriteLine(database.Name); // 小新11 // 获取当前内存的值,就是造成并发异常的值 var current = (Student)tracking.CurrentValues.ToObject(); Console.WriteLine(current.Name); // 小新22 tracking.OriginalValues.SetValues(database); //tracking.GetDatabaseValues().SetValues(current); ctx2.SaveChanges(); // 需要调用savechanges方法 throw ex; } }
这里有一个疑问,照我的理解应该是将current的值赋值给当前数据库中的值,也就是tracking.GetDatabaseValues().SetValues(current);
但是这样写报错,虽然作者也专门解释了,但是我还是懵的……
行吧,这个还是必要自己去动手弄一下,体会一下。初级版的并发冲突解决方案就到这里了。
后面还是不得不说一下,我也是今天才知道多个using可以这个很简单的堆叠起来写,很优雅啊。
然后利用上下文的日志打印真的很有用。
using (DB1_Context ctx1 = new DB1_Context()) using (DB1_Context ctx2 = new DB1_Context()) { ctx1.Database.Log = msg => Console.WriteLine("ctx111111111111111:" + msg); ctx2.Database.Log = msg => Console.WriteLine("ctx222222222222222:" + msg); var stu1 = ctx1.Students.FirstOrDefault(); var stu2 = ctx2.Students.FirstOrDefault(); stu1.Name = "小新11"; stu2.Name = "小新22"; ctx1.SaveChanges(); ctx2.SaveChanges(); }
从打印的结果可以看到,关于数据库初始化的任务全部是由ctx1去执行的,就是这些什么Migration这些东西
难道是我ctx1对象先构造的问题?或者ctx1的log先打印的问题,于是我改成ctx2先构造,然后ctx2的log也先执行,发现还是上面打印的结果,还是ctx1去执行数据库初始化的工作。
直到我将ctx2先查询出student对象才变成ctx2先执行这些操作。所以是不是就认识到,多个上下文到底是谁来负责数据库初始化的任务呢?那就看看是谁先与数据库交互了,现在构造上下文对象这里并没有与数据库发生交互。
行吧,就这了,后面还会继续学习。