ReadUnCommitted与ReadCommitted

转载至:http://www.cnblogs.com/zhenyulu/articles/330494.html

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不起作用,进程也不会被阻塞,不过确读到了"脏"数据。

原文地址:https://www.cnblogs.com/jshchg/p/2122115.html