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