提到数据库操作,特别是企业级的数据库应用,就不得不提一个多人操作时经常会产生的问题——并发冲突。本文首先来看一下什么是并发冲突,传统的并发冲突有现有的处理方式,最后,结合EF,看一个处理并发冲突的实例。
一、要完成本文中的实例,您需要作如下准备:
- 将Visual Studio 2008及.NET Framework 3.5升级到SP1。点击转到升级地址。
- 安装SQL SERVER 2005,VS 2008中自带的EXPRESS版的SQL SERVER应该也可以用。
- 下载并附加数据库:点击下载DemoDbV2。
- 创建一个VB Console Application,并且取一个合适的名字(例如:Concurrency之类的)。注意,目标Framework要设置成3.5版。
二、什么是并发冲突
让我们来看一个跟取款相关的例子:某年某月某日某时某分,老王在A取款机取钱,他儿子小王同时在B取款机取钱(不要问我为什么这么巧^_^),他俩从同一个账号上取。于是就发生了如下一序列的操作:
A取款机向中央数据库提问:这账上还有多少钱?
B取款机向中央数据库询问:这账上还有多少钱?
中央数据库回答A取款机:2W。
中央数据库回答B取款机:2W。
然后,老王对A取款机说:我要取出1.5W。
同时,小王对B取款机说:我要取出1.8W。
A取款机就算了一下,2W-1.5W=0.5W>0,于是就吐出1.5W现金给了老王,并且准备告诉中央数据库,现在还剩0.5W啦。但是,就在它告诉中央数据库之前,发生了以下的事情:
B取款机计算了一下,2W(此时,它还不知道余额已经成0.5W了,因为A取款机还没有告诉中央数据库)减去1.8W等于0.2W大于0,于是就吐出1.8W现金给了小王。然后,它当然也要知会中央数据库。
中央数据库于是收到A取款机的消息,说,这个账号还剩0.5W,于是刷新余额为0.5W。然后又收到B取款机说还剩0.2W,于是,就刷新余额为0.2W。
呵呵,于是,小王+老王的账户里一共存有2W元,结果老王取了1.5W元,小王取了1.8W元,账户里却还剩了0.2W元!~@#$%^&
图1
这就是一种并发冲突,由于同一时间有两个或者多个端在对同一数据进行操作,从而导致数据发生了错误。如果取款机真的以这样的方式来处理并发,那么,我现在就不写这片文章了——赶紧发动全家对表,说好了在某一时刻同时取钱去^_^
三、常见的并发冲突处理方式
一般来说,我们把并发冲突处理方式归结为3类。
第一类:放任不管方式;第二类:开放式并发处理方式;第三类:保守式并发处理方式。
1. 放任不管方式:
与其说这是一种处理并发冲突的方式,不如说,它是一种没有对并发冲突做任何处理的方式。但是在许多过去的系统里,由于没有考虑到多用户、网络应用等情况,这种"处理方式"还真存在于不少系统中。
举例来说,A、B两人从数据库中获取了同一个笔记本的信息,例如:IBM ThinkPad T61吧。然后:A把牌子改成了:Lenovo ThinkPad,B把型号改成了T61 8890A24。然后,他们开始提交了。此时,如果A先提交,然后B提交,那么,最后的结果是:IBM ThinkPad T61 8890A24;反之,则变成Lenovo ThinkPad T61。
总之一句话,谁最后提交谁老大。想像一下,如果A修改了1000个属性的值,B修改了1个属性的值,那么,对于先提交的A来说,这将是一个多么惨痛的打击:-)
虽然这种放任不管的方式似乎不太负责任,但是,其处理性能却是相对较高的。
2. 开放式并发处理
开放式并发处理,老外叫做Optimistic Concurrency——乐观的并发。这种并发处理方式要求我们对并发抱有一种乐观的态度:百分之九十九点九九不会发生并发冲突,万一发生了,系统也能捕获到冲突,或者根据策略自动处理,或者,就提醒一下用户,让用户来决定是不是要继续提交。
仍然用上面的例子来说这事儿:A、B两个人同时获取了笔记本的信息:IBM ThinkPad T61。然后……(此处跟上例做一样的修改,直到提交)此时,如果A先提交,那么,B提交的时候,系统会发现,哎哟,不好,有并发冲突了,就会抛个异常给B,让B知道,发生并发冲突了,然后,B就可以根据实际情况,选择相应的处理策略(比如,继续提交进行覆盖或者取消提交等等);相反,如果B先提交,那么,A提交时,就会得到相应的提醒。
这样的并发处理方式,可以说在可靠性与性能上取得平衡,适合于对数据可靠性要求不是特别严格,需要较高的性能,并且不会大量发生并发的场合。
3. 保守式并发处理
这是最为严谨的一种并发冲突的处理方式。它把并发转化为了串行操作。
例如,A从数据库中获取了笔记本信息:IBM ThinkPad T61,B也要对其进行修改,但此时由于A已经从数据库中将数据取出,因此,B被置于等待状态。直到A把数据修改完提交了,数据库数据更新为Lenovo ThinkPad T61了,此时,数据库才把数据给B,那么B就可以在Lenovo ThinkPad T61的基础上,把它修改为Lenovo ThinkPad T61 8890A24。而在B提交前,其它一切针对此记录的操作都得排除等着B。
这样子当然非常理想,由于不存在并发,自然也就消除了并发冲突的问题。但是,这种锁也存在着较为隐蔽的风险:如果A修改了数据,一直不提交,或者A因为故障,没有办法提交,那么,其它所有的相关的操作,都将被阻碍住。因此,只有对数据准确性要求极高并且用户可以忍受等待的情况下,使用这种并发冲突的处理方法。
四、EF并发冲突处理实例
EF发布时,提供了两种并发冲突处理方式:放任不管方式和开放式并发。默认采用放任不管的方式处理。
如果要使用开放式并发,那么,必须设置相应属性上的Concurrency Mode值为Fixed。我们先对实体类的属性进行修改,让其支持开放式并发,然后来模拟一个并发的序列,看看怎么来处理并发冲突。
当前数据库情况如图2所示:
图2
图3
第一步,在概念模型设计器里,按照图3所示,分别把Notebook.Brand和Notebook.Type属性的Concurrency Mode设置为Fixed。
然后,我们写一段代码来模拟一个并发冲突的情况:
'Create object context Dim objContext1 As New NbWhEntities() Dim objContext2 As New NbWhEntities()
'Query the same record as 2 entities Dim laptop1 = (From aLaptop In objContext1.Notebook _ Where aLaptop.Id = 1 _ Select aLaptop).FirstOrDefault()
Dim laptop2 = (From aLaptop In objContext2.Notebook _ Where aLaptop.Id = 1 _ Select aLaptop).FirstOrDefault()
'Modify the entity laptop1.Brand = "Lenovo ThinkPad" laptop2.Type = laptop2.Type & " 8890A24"
'Submit 1st object context objContext1.SaveChanges()
Try 'Submit 2nd object context and cause cuncurrency exception objContext2.SaveChanges() Catch ex As OptimisticConcurrencyException 'Using refresh method to objContext2.Refresh(Objects.RefreshMode.StoreWins, laptop2) 'We should load the new data from db and ask user to change it again objContext2.SaveChanges() End Try |
我们创建了两个Object Context,分别查询出了同一个实体,第一个修改其品牌为"Lenovo ThinkPad",第二个同时将其型号修改为"T61 8890A24"。然后,第一个实体保存;然后,第二个保存。由于我们在Brand属性上设置了Concurrency Mode为Fixed,而此时,laptop2中的Brand属性的值应该是一开始取得的T61,而数据库里的值是"Lenovo T61",于是,系统就会抛出"OptimisticConcurrencyException"(开放式并发异常)。当程序捕获到异常以后,就可以使用Object Context的Refresh方法对异常采取处理。由于没有在刷新laptop2以后,未对其作任何修改,故最终结果将与laptop1提交时的结果一致。
这里,Refresh的第一个参数值得注意一下,它是一个枚举值,有两个选项:StoreWins或者是ClientWins。见名知义,如果是StoreWins,那么,Refresh以后,laptop2的值将与数据库里的对应记录的值一致(修改会丢失);而如果ClientWins,则laptop2的值保持,并且提交以后,会把objContext1提交的修改覆盖。
其实,这两种方法均不完美,总会导致一部分修改丢失。但是,这总比在不知情的情况下的覆盖要好。
另外,需要说明,上面的方法,只是对并发冲突的一种模拟,这样的模式,在处理并发冲突时会有问题。一般的处理方法是,当检测到并发冲突时,提示用户会重新从数据库载入数据,然后,让用户在新数据的情况下重新修改后再次提交,直到不再有并发冲突发生。
这样,看似可能成为一个无穷尽的痛苦的过程,但实际上,由于这种处理方式是基于对并发冲突的乐观估计来设计的,因此,当我们认为并发冲突很少有可能发生时,这种处理方式可以有效避免数据被无意识的覆盖问题。
五、示例代码下载
点击下载。
Little knowledge is dangerous.