来到并发这里了,我自己得先承认,并发对我来说完全是一个熟悉又真正陌生的东西,总的来说,我对并发一无所知。
那么不管是怎么回事,我也要说一下。之前看过零星的一些讲硬件的东西,说的是,很多个应用你看似同时开启,同时运行的,其实只是,CPU速度太快,让你察觉不了。
所以不可能存在两个任务同时进行,这只是错觉。所以我现在给自己一些自信,我断定!不存在,就像一个啤酒瓶,口就那么大,一次只容许一颗珠子进去,不可能两个同时进去,都是错觉!
来看EF中的并发。
我们在使用EF上下文时,遵循的是一个请求对应一个上下文,对事务也是这个态度,不要事务那么长,越短越好。
一个请求对应一个上下文,那么服务器同时接受到了多个请求,构造出多个上下文对象,针对同一资源操作,问题就出来这里。
因为不同的上下文中查询出的实体都是各自的,并不是同一个引用。
这里有两个上下文,都得到了名叫“张三”的学生实体,第一个上下文修改为“李四”,第二个上下文修改为“张三”,那么最终的结果应该是“张三”,但是看看下面的代码,其实最终数据库的结果是“李四”
using(DB1_Contextctx1=newDB1_Context())using(DB1_Contextctx2=newDB1_Context()){varstu1=ctx1.Students.FirstOrDefault();varstu2=ctx2.Students.FirstOrDefault();stu1.Name="李四";stu2.Name="张三";ctx1.SaveChanges();ctx2.SaveChanges();}
你觉得应该是第一个上文查询修改完,再第二个上下文接着查询修改就行了。但是高并发的情况下是无法保证的。
那么我们看下一个上下文中查询相同的两个实体。引用是相等的。所以整个解决方案就使用一个上下文是不是就行了?我觉得是,但是这是不科学的。
using(DB1_Contextctx=newDB1_Context()){varstu1=ctx.Students.FirstOrDefault();varstu2=ctx.Students.FirstOrDefault();Console.WriteLine(ReferenceEquals(stu1,stu2));//TrueConsole.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
现在来用try/catch就可以捕获了
这个异常我之前没有学到这里来的时候碰到过,没有记录下来当时是写的什么代码,真可惜!
来看看行版本的方式。这就需要为实体添加一个字节数组类型的属性,并且该属性需要配置
那么接下来我们就开始在异常处理中进行操作,他不是数据不是最新的吗?那么我就让他得到最新的。因为EF中有针对并发异常的类(DbUpdateConcurrencyException)。
DbUpdateConcurrencyException中具有Entries属性,该属性返回一系列DbEntityEntry对象,表示冲突实体的跟踪信息。
using(DB1_Contextctx1=newDB1_Context())using(DB1_Contextctx2=newDB1_Context()){varstu1=ctx1.Students.FirstOrDefault();varstu2=ctx2.Students.FirstOrDefault();stu1.Name="小新111";ctx1.SaveChanges();stu2.Name="小新222";try{ctx2.SaveChanges();}catch(DbUpdateConcurrencyExceptionex){vars=ex.Entries.Single();s.Reload();Console.WriteLine("stu2.Name:"+stu2.Name);//小新11stu2.Name="小新222";ctx2.SaveChanges();throwex;}}
调用Reload方法来刷新数据库中的最新值到当前内存中的值,就是造成并发冲突的这个对象,更新它。
如果说不用Relod,也有另外一种方式来实现
这里有一个疑问,照我的理解应该是将current的值赋值给当前数据库中的值,也就是tracking.GetDatabaseValues().SetValues(current);
但是这样写报错,虽然作者也专门解释了,但是我还是懵的……
行吧,这个还是必要自己去动手弄一下,体会一下。初级版的并发冲突解决方案就到这里了。
后面还是不得不说一下,我也是今天才知道多个using可以这个很简单的堆叠起来写,很优雅啊。
然后利用上下文的日志打印真的很有用。
using(DB1_Contextctx1=newDB1_Context())using(DB1_Contextctx2=newDB1_Context()){ctx1.Database.Log=msg=>Console.WriteLine("ctx111111111111111:"+msg);ctx2.Database.Log=msg=>Console.WriteLine("ctx222222222222222:"+msg);varstu1=ctx1.Students.FirstOrDefault();varstu2=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先执行这些操作。所以是不是就认识到,多个上下文到底是谁来负责数据库初始化的任务呢?那就看看是谁先与数据库交互了,现在构造上下文对象这里并没有与数据库发生交互。