spring.net事务配置:
isolation="ReadCommitted"
isolation="ReadUnCommitted"
这两个分别是什么意义???!!!
ReadUnCommitted与ReadCommitted
ReadUnCommitted是最低的隔离级别,这个级别的隔离允许读入别人尚未提交的脏数据,除此之外,在这种事务隔离级别下还存在不可重复读的问题。
ReadCommitted是许多数据库的缺省级别,这个隔离级别上,不会出现读取未提交的数据问题,但仍然无法避免不可重复读(包括幻影读)的问题。当你的系统对并发控制的要求非常严格时,这种默认的隔离级别可能无法提供数据有效的保护,但对于决大多数应用来讲,这种隔离级别就够用了。
我们使用下面的实验来进行测试:
首先配置SQL Server 2000数据库,附加DBApp数据库。然后在Visual Studio .net中建立一管理控制台应用程序,添加必要的命名空间引用:
using System;using System.Data;using System.Data.SqlClient;using System.Configuration;
然后建立两个数据库链接,并分别采用不同的事务隔离级别:
private static SqlConnection conn1;private static SqlConnection conn2;private static SqlTransaction tx1;private static SqlTransaction tx2;private static void Setup(){conn1 = new SqlConnection(connectionString);conn1.Open();tx1 = conn1.BeginTransaction(IsolationLevel.ReadUncommitted);conn2 = new SqlConnection(connectionString);conn2.Open();tx2 = conn2.BeginTransaction(IsolationLevel.ReadCommitted);}
其中事务1允许读入未提交的数据,而事务2只允许读入已提交数据。
在主程序中,我们模拟两个人先后的不同操作,以产生并发一致性问题:
public static void Main(){Setup();try{ReadUnCommittedDataByTransaction1();UnCommittedUpdateByTransaction2();ReadUnCommittedDataByTransaction1();tx2.Rollback();Console.WriteLine("\n-- Transaction 2 rollbacked!\n");ReadUnCommittedDataByTransaction1();tx1.Rollback();}catch{……}}
第一步,使用ReadUnCommittedDataByTransaction1方法利用事务1从数据库中读入id值为1的学生信息。此时的信息是数据库的初始信息。
第二步,调用UnCommittedUpdateByTransaction2方法,从第2个事务中发送一UPDATE命令更新数据库,但尚未提交。
第三步,再次调用ReadUnCommittedDataByTransaction1,从事务1中读取数据库数据,你会发现由事务2发布的尚未提交的更新被事务1读取出来(ReadUnCommitted)。
第四步,事务2放弃提交,回滚事务tx2.Rollback();。
第五步,再次调用ReadUnCommittedDataByTransaction1();,读取数据库中的数据,此次是已经回滚后的数据。
程序运行结果如下:
-- Read age from database:Age:20-- Run an uncommitted command:UPDATE student SET age=30 WHERE id=1-- Read age from database:Age:30-- Transaction 2 rollbacked!-- Read age from database:Age:20
关于ReadUnCommittedDataByTransaction1()与UnCommittedUpdateByTransaction2()的方法定义如下:
private static void UnCommittedUpdateByTransaction2(){string command = "UPDATE student SET age=30 WHERE id=1";Console.WriteLine("\n-- Run an uncommitted command:\n{0}\n", command);SqlCommand cmd = new SqlCommand(command, conn2);cmd.Transaction = tx2;cmd.ExecuteNonQuery();}private static void ReadUnCommittedDataByTransaction1(){Console.WriteLine("-- Read age from database:");SqlCommand cmd = new SqlCommand("SELECT age FROM student WHERE id = 1", conn1);cmd.Transaction = tx1;try{int age = (int)cmd.ExecuteScalar();Console.WriteLine("Age:{0}", age);}catch(SqlException e){Console.WriteLine(e.Message);}}
从上面的实验可以看出,在ReadUnCommitted隔离级别下,程序可能读入未提交的数据,但此隔离级别对数据库资源锁定最少。
本实验的完整代码可以从"SampleCode\Chapter 2\Lab 2-6"下找到。
让我们再来做一个实验(这个实验要求动作要快的,否则可能看不到预期效果)。首先修改上面代码中的Setup()方法代码,将
tx1 = conn1.BeginTransaction(IsolationLevel.ReadUncommitted);
改为:
tx1 = conn1.BeginTransaction(IsolationLevel.ReadCommitted);
再次运行代码,你会发现程序执行到第三步就不动了,如果你有足够的耐心等下去的话,你会看到"超时时间已到。在操作完成之前超时时间已过或服务器未响应。"的一条提示,这条提示究竟是什么意思呢?让我们探察一下究竟发生了什么:
第一步,在做这个实验之前,先将SQL Server 2000的企业管理器打开,然后再将SQL Server事件探察器打开并处于探察状态。
第二步,运行改动后的程序,程序执行到一半就暂停了。此时迅速切换到企业管理器界面,右击"管理"下面的"当前活动",选择"刷新"(整个过程应在大约15秒内完成即可,如图 2-8所示),我们便得到了数据库当前进程的一个快照。
图 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不起作用,进程也不会被阻塞,不过确读到了"脏"数据。