读书人

丢掉的修改、不可重复读、读脏数据、幻

发布时间: 2012-09-07 10:38:15 作者: rapoo

丢失的修改、不可重复读、读脏数据、幻影读

?

图?2-8?使用企业管理器查看当前活动

我们发现此时进程出现了阻塞,被阻塞者是52号进程,而阻塞者是53号进程。也就是说53号进程的工作妨碍了52号进程继续工作。(不同实验时进程号可能各不相同)

第三步,为了进一步查明原因真相,我们切换到事件探察器窗口,看看这两个进程都是干什么的。如图?2-9所示,事件探察器显示了这两个进程的详细信息。从图中我们可以看出,52号进程对应我们的事务1,53号进程对应我们的事务2。事务2执行了UPDATE命令,但尚未提交,此时事务1去读尚未提交的数据便被阻塞住。从图中我们可以看出52号进程是被阻塞者。

此时如果事务2完成提交,52号进程便可以停止等待,得到需要的结果。然而我们的程序没有提交数据,因此52号进程就要无限等下去。所幸SQL?Server?2000检测到事务2的运行时间过长(这就是上面的错误提示"超时时间已到。在操作完成之前超时时间已过或服务器未响应。"),所以将事务2回滚以释放占用的资源。资源被释放后,52号进程便得以执行。


丢掉的修改、不可重复读、读脏数据、幻影读

?

图?2-9?事件探察器探察阻塞命令

第四步,了解了上面发生的事情后,我们现在可以深入讨论一下共享锁和排它锁的使用情况了。重新回到企业管理器界面,让我们查看一下两个进程各占用了什么资源。从图?2-10中我们可以看出,53号进程(事务2)在执行更新命令前对相应的键加上了排它锁(X锁),按照前文提到的1级封锁协议,该排它锁只有在事务2提交或回滚后才释放。现在52号进程(事务1)要去读同一行数据,按照2级封锁协议,它要首先对该行加共享锁,然而?该行数据已经被事务2加上了排它锁,因此事务1只能处于等待状态,等待排它锁被释放。因此我们就看到了前面的"阻塞"问题


丢掉的修改、不可重复读、读脏数据、幻影读

?

图?2-10?进程执行写操作前首先加了排它锁


丢掉的修改、不可重复读、读脏数据、幻影读

?

?

图?2-11?进程读操作前要加共享锁,但被阻塞

?

当事务1的事务隔离级别是ReadUnCommitted时,读数据是不加锁的,因此排它锁对ReadUnCommitted不起作用,进程也不会被阻塞,不过确读到了"脏"数据。

2.2.3.2?RepeatableRead

RepeatableRead是指可重复读,它的隔离级别要比ReadCommitted级别高。它允许某事务执行重复读时数据保持不变,但是仍然无法解决幻影读问题。为了更深入的了解RepeatableRead所能解决的问题,我们还是使用下面的实验来加以印证:

第一步,事务1与事务2同时设置为ReadCommitted,并同时开启事务。

?

private?static?void?Setup(){conn1?=?new?SqlConnection(connectionString);conn1.Open();tx1?=?conn1.BeginTransaction(IsolationLevel.ReadCommitted);conn2?=?new?SqlConnection(connectionString);conn2.Open();tx2?=?conn2.BeginTransaction(IsolationLevel.ReadCommitted);}

第二步,事务1读取数据库中数据。注意此时并没有通过提交或回滚的方式结束事务1,事务1仍然处于活动状态。

private?static?int?ReadAgeByTransaction1(){return?(int)ExecuteScalar("SELECT?age?FROM?student?WHERE?(id?=?1)");}private?static?object?ExecuteScalar(string?command){Console.WriteLine("--?Execute?command:?{0}",?command);SqlCommand?cmd?=?new?SqlCommand(command,?conn1);cmd.Transaction?=?tx1;return?cmd.ExecuteScalar();}

第三步,事务2修改年龄数据并提交修改。

private?static?void?ModifyAgeByTransaction2(){string?command?=?"UPDATE?student?SET?age=30?WHERE?id=1";Console.WriteLine("--?Modify?age?by?transaction2,?command:{0}",?command);SqlCommand?cmd?=?new?SqlCommand(command,?conn2);cmd.Transaction?=?tx2;try{cmd.ExecuteNonQuery();tx2.Commit();}catch(Exception?e){Console.WriteLine(e.Message);tx2.Rollback();}}

第四步,事务1重复读取年龄数据,此时会发现读取出来的数据是修改过的数据,与上次读取的数据不一样了!顾名思义,不可重复读。主程序代码如下:

public?static?void?Main(){Setup();try{int?age1?=?ReadAgeByTransaction1();ModifyAgeByTransaction2();int?age2?=?ReadAgeByTransaction1();Console.WriteLine("\nFirst?Read:?age={0}\nSecond?Read:?age={1}",?age1,?age2);}catch(Exception?e){Console.WriteLine("Got?an?error!?"?+?e.Message);}finally{CleanUp();}}

程序的运行结果如下:

--?Execute?command:?SELECT?age?FROM?student?WHERE?(id?=?1)--?Modify?age?by?transaction2,?command:UPDATE?student?SET?age=30?WHERE?id=1--?Execute?command:?SELECT?age?FROM?student?WHERE?(id?=?1)First?Read:?age=20Second?Read:?age=30

之所以出现了重复读时读取的数据与第一次读取的不一样,是因为事务1被设置成了ReadCommitted隔离类型,该隔离级别无法防止不可重复读的问题。要想在一个事务中两次读取数据完全相同就必须使用RepeatableRead事务隔离级别。

让我们修改上面的Setup()方法中的代码,将事务1的隔离级别设置为RepeatableRead:

tx1?=?conn1.BeginTransaction(IsolationLevel.RepeatableRead);

再次运行该程序,你会发现程序执行到第二步就暂停了,如果等待一段时间后你就会看到"超时时间已到。在操作完成之前超时时间已过或服务器未响应。"的错误提示,此时,重复读的数据确和第一次读完全一样。程序执行结果如下:

--?Execute?command:?SELECT?age?FROM?student?WHERE?(id?=?1)--?Modify?age?by?transaction2,?command:UPDATE?student?SET?age=30?WHERE?id=1超时时间已到。在操作完成之前超时时间已过或服务器未响应。--?Execute?command:?SELECT?age?FROM?student?WHERE?(id?=?1)First?Read:?age=20Second?Read:?age=20

为了探明原因,还是象上一个案例一样,再次执行该程序,当出现暂停时迅速切换到企业管理器中查看当前活动的快照,并检查阻塞进程中数据锁定情况,你会发现如图?2-12和图?2-13所示的内容:


丢掉的修改、不可重复读、读脏数据、幻影读

?

图?2-12?RepeatableRead在读数据时加S锁,直到事务结束才释放

?


丢掉的修改、不可重复读、读脏数据、幻影读

图?2-13?修改数据要求加X锁,但被阻塞

根据3级封锁协议,事务T在读取数据之前必须先对其加S锁,直到事务结束才释放。因此,事务1在第一次读取数据时便对数据加上了共享锁,第一次数据读取完成后事务并未结束,因此该共享锁并不会被释放,此时事务2试图修改该数据,按照2级封锁协议,在写之前要加排它锁,但数据上的共享锁尚未被释放,导致事务2不得不处于等待状态。当事务2等待时间超时后,SQL?Server就强制将该事务回滚。尽管事务2执行失败,但保证了事务1实现了可重复读级别的事务隔离。

RepeatableRead事务隔离级别允许事务内的重复读操作,但是这并不能避免出现幻影读问题,如果您的程序中存在幻影读的潜在问题的话,就必须采用最高的事务隔离级别:Serializable。

2.2.3.3?Serializable

Serializable隔离级别是最高的事务隔离级别,在此隔离级别下,不会出现读脏数据、不可重复读和幻影读问题。在详细说明为什么之前首先让我们看看什么是幻影读

所谓幻影读是指:事务1按一定条件从数据库中读取某些数据记录后,事务2插入了一些符合事务1检索条件的新记录,当事务1再次按相同条件读取数据时,发现多了一些记录。让我们通过以下案例来重现幻影读问题

第一步,将事务1和事务2均设为RepeatableRead隔离级别,并同时开启事务。

private?static?void?Setup(){conn1?=?new?SqlConnection(connectionString);conn1.Open();tx1?=?conn1.BeginTransaction(IsolationLevel.RepeatableRead);conn2?=?new?SqlConnection(connectionString);conn2.Open();tx2?=?conn2.BeginTransaction(IsolationLevel.RepeatableRead);}

第二步,事务1读取学号为1的学生的平均成绩以及所学课程的门数。此时读到学生1学了3门课程,平均成绩为73.67。注意,此时事务1并未提交。

private?static?double?ReadAverageMarksByTransaction1(){return?(double)ExecuteScalar("SELECT?AVG(mark)?AS?AvgMark?FROM?SC?WHERE?(id?=?1)");}private?static?int?ReadTotalCoursesByTransaction1(){return?(int)ExecuteScalar("SELECT?COUNT(*)?AS?num?FROM?SC?WHERE?(id?=?1)");}private?static?object?ExecuteScalar(string?command){Console.WriteLine("--?Execute?command:?{0}",?command);SqlCommand?cmd?=?new?SqlCommand(command,?conn1);cmd.Transaction?=?tx1;return?cmd.ExecuteScalar();}

第三步,事务2向数据库插入一条新记录,让学号为1的同学再学1门课程,成绩是80。然后提交修改到数据库。

private?static?void?InsertRecordByTransaction2(){string?command?=?"INSERT?INTO?SC?VALUES(1,?5,?80)";Console.WriteLine("--?Insert?to?table?SC?by?transaction?2");Console.WriteLine("--?Command:{0}\n",?command);SqlCommand?cmd?=?new?SqlCommand(command,?conn2);cmd.Transaction?=?tx2;try{cmd.ExecuteNonQuery();tx2.Commit();}catch(Exception?e){Console.WriteLine(e.Message);tx2.Rollback();}}

第四步,事务1再次读取学号为1的学生的平均成绩以及所学课程的门数。此时读到确是4门课程,平均成绩为75.25。与第一次读取的不一样!居然多出了一门课程,多出的这门课程就像幻影一样出现在我们的面前。测试用主程序如下:

public?static?void?Main(){Setup();try{Console.WriteLine(">>>>?Step?1");double?avg?=?ReadAverageMarksByTransaction1();int?total?=?ReadTotalCoursesByTransaction1();Console.WriteLine("avg={0,5:F2},?total={1}\n",?avg,?total);Console.WriteLine(">>>>?Step?2");InsertRecordByTransaction2();Console.WriteLine(">>>>?Step?3");avg?=?ReadAverageMarksByTransaction1();total?=?ReadTotalCoursesByTransaction1();Console.WriteLine("avg={0,5:F2},?total={1}\n",?avg,?total);}catch(Exception?e){Console.WriteLine("Got?an?error!?"?+?e.Message);}finally{CleanUp();}}

程序执行结果如下:

>>>>?Step?1--?Execute?command:?SELECT?AVG(mark)?AS?AvgMark?FROM?SC?WHERE?(id?=?1)--?Execute?command:?SELECT?COUNT(*)?AS?num?FROM?SC?WHERE?(id?=?1)avg=73.67,?total=3>>>>?Step?2--?Insert?to?table?SC?by?transaction?2--?Command:INSERT?INTO?SC?VALUES(1,?5,?80)>>>>?Step?3--?Execute?command:?SELECT?AVG(mark)?AS?AvgMark?FROM?SC?WHERE?(id?=?1)--?Execute?command:?SELECT?COUNT(*)?AS?num?FROM?SC?WHERE?(id?=?1)avg=75.25,?total=4

大家可以思考一下,为什么RepeatableRead隔离模式并不能使得两次读取的平均值一样呢?(可以从锁的角度来解释这一现象)。

仍然象前面的做法一样,我们看看究竟发生了什么事情。在探察之前,先将Setup方法中事务1的隔离级别设置为Serializable,再次运行程序,当发现程序运行暂停时,查看数据库当前活动快照,你会发现如图?2-14和图?2-15所示的锁定问题


丢掉的修改、不可重复读、读脏数据、幻影读

?

图?2-14?Serializable隔离模式对符合检索条件的数据添加了RangeS-S锁


丢掉的修改、不可重复读、读脏数据、幻影读

?

图?2-15?当试图插入符合RangeIn条件的记录时,只能处于等待状态

从图中我们可以看出,在Serializalbe隔离模式下,数据库在检索数据时,对所有满足检索条件的记录均加上了RangeS-S共享锁。事务2试图去插入一满足RangeIn条件的记录时,必须等待这些RangS-S锁释放,否则就只能处于等待状态。在等待超时后,事务2就会被SQL?Server强制回滚。

修改后的程序运行结果如下:

>>>>?Step?1--?Execute?command:?SELECT?AVG(mark)?AS?AvgMark?FROM?SC?WHERE?(id?=?1)--?Execute?command:?SELECT?COUNT(*)?AS?num?FROM?SC?WHERE?(id?=?1)avg=73.67,?total=3>>>>?Step?2--?Insert?to?table?SC?by?transaction?2--?Command:INSERT?INTO?SC?VALUES(1,?5,?80)超时时间已到。在操作完成之前超时时间已过或服务器未响应。>>>>?Step?3--?Execute?command:?SELECT?AVG(mark)?AS?AvgMark?FROM?SC?WHERE?(id?=?1)--?Execute?command:?SELECT?COUNT(*)?AS?num?FROM?SC?WHERE?(id?=?1)avg=73.67,?total=3

事务2的运行失败确保了事务1不会出现幻影读问题。这里应当注意的是,1、2、3级封锁协议都不能保证有效解决幻影读问题

?

?

?

?

2.3?建议

通过上面的几个例子,我们更深入的了解了数据库在解决并发一致性问题时所采取的措施。锁机制属于最底层的保证机制,但很难直接使用。我们可以通过不同的事务隔离模式来间接利用锁定机制确保我们数据的完整一致性。在使用不同级别的隔离模式时,我们也应当注意以下一些问题

一般情况下ReadCommitted隔离级别就足够了。过高的隔离级别将会锁定过多的资源,影响数据的共享效率。?你所选择的隔离级别依赖于你的系统和商务逻辑。?尽量避免直接使用锁,除非在万不得已的情况下。?我们可以通过控制WHERE短语中的字段实现不同的更新策略,防止出现丢失的修改问题。但不必要的更新策略可能造成SQL命令执行效率低下。所以要慎用时间戳和过多的保护字段作为更新依据。

读书人网 >其他数据库

热点推荐