事务是一个工作单元,可能包含查询和修改数据以及修改数据定义等多个活动。我们可以显式或隐式的定义事务边界。可以使用BEGIN TRAN或者BEGIN TRANSACTION语句显式的定义事务的开始。如果希望提交事务,可以使用COMMIT TRAN语句显式的定义事务结束。如果不希望提交事务(即要撤销更改),可以使用ROLLBACK TRAN或者ROLLBACK TRANSACTION语句-摘抄自SQL Server 2012基础教程。
如果不显式的标记事务的边界,默认情况下,SQL Server将把每个单独语句作为一个事务,换句话说,默认情况下,每个单独语句结束后SQL Server自动提交事务。
说到事务就联想到并发,为了解决事务中的并发我们则不得不讨论下锁,所以接下来我们首先熟悉一下锁的模式-排他锁和共享锁。
先看看下面的试验,先开始一个事务处理,这个事务不提交也不回滚,然后另外在打开一个窗口查询数据。
在上面的事务没有提交也没有回滚的情况下,如果另一个程序在执行查询,那么就会阻塞,如果没有设置查询超时,就会一直等待。
原因:当事务会话对表进行修改(增删改)时,事务会请求数据资源的一个排他锁,这个锁直到事务完成(提交或者回滚)才会被取消。当读取数据时需要获取该资源上的共享锁,但是排他锁和共享锁不能兼容,此时会导致查询阻塞不得不进行等待。
就上面的试验,第一个事务拥有排它锁,在事务没有完成的情况下,第二个事务携带共享锁来访问表,此时就必须等待第一个事务完成后才能进行查询。但是反过来没问题,就是先有查询的事务进行查询,但是没完成事务,然后第二个事务来进行修改
并且能正常完成事务(这主要是因为在默认的隔离级别下,共享锁在完成数据的读取后就释放了,而不是在事务会话完成后释放)。注意,这个锁是针对修改的数据的行的,不是针对整个表。比如上面修改的是id=1的行,但如果我查询id=2的行,那么就不会出现阻塞。
因此,在事务处理中,如果遇到并发的情况,会有可能导致程序阻塞。
锁的隔离级别
SQL Server支持4个基于悲观并发控制的传统隔离级别:
READ UNCOMMITTED、
READ COMMITTED(企业内部部署的SQL Server实例的默认方式)、
REPEATABLE READ、
SERIALIZABLE。
SQL Server还支持两种基于并发控制(行版本)的隔离级别:
SNAPSHOT
READ COMMITTED SNAPSHOT(SQL Database的默认方式)
在某种意义上,SNAPSHOT和READ COMMITTED SNAPSHOT分别是READ COMMITTED和SERIALIZABLE的乐观并发对应方式。
第一类丢失更新:撤销一个事务时,把其他事务已提交的更新数据覆盖。
脏读:一个事务读到另一事务未提交的更新数据。
虚读:一个事务读到另一事务已提交的新插入的数据。
不可重复读:一个事务读到另一事务已提交的更新数据。
第二类丢失更新:这是不可重复读中的特例,一个事务覆盖另一事务已提交的更新数据。
READ UNCOMMITTED隔离级别
READ UNCOMMITTED是最低隔离级别,在该隔离级别中,读取者不需要请求共享锁,不要求共享锁的读取者从不会与持有排他锁的写入者发生冲突,这意味着读取者可以读取写入者未提交的更改即脏读,也就是说,读取者不会干扰要求了排他锁的写入者,在该隔离级别下运行的写入者更改数据时,读取者可以读取数据。
默认情况下,上面的语句执行成功的,但是事务没有执行完成,这时排它锁是存在的,下面在另一个事务中设置隔离级别为READ UNCOMMITTED运行如下代码,可以看到,这个事务能查询到上一个事务没有提交的修改
这时候我们再将第一个事务回滚(只执行红框中的语句)。
可以看到在隔离级别是READ UNCOMMITTED的情况下,事务并发会出大问题的(第二个事务获取了一个错误的数据)。
READ COMMITTED隔离级别
该隔离级别仅允许读取已提交的更改。它通过要求读取者获得一个共享锁来防止未提交的读取,也就是说,如果一个写入者持有了排他锁,读取者的共享锁请求将会与写入者的排它锁冲突,此时必须等待,一旦写入者提交了事务,读取者就可以获得它的共享锁,所以它必然是只读取提交后的修改。这个已经在本文最开始的时候试验过了。
这里再次试验一下:虚读
在第一个事务中,第一次查询获得了6条数据,这时候事务未提交,开始第二个事务
成功提交后,继续执行第一个事务的第二次查询,
不可重复读和上面的试验类似,只是变成的修改。但是对于虚读,我一直没理解真实的场景中有什么问题。看了很多介绍,都说在同一个事务中,两次相同条件的查询获得的结果不同,就是虚读或者不可重复读,但是这样的情况在实际场景
中是很合理的啊。所以有点不明白。很多介绍中说是,在同一个事务中,两次查询获得的结果不一致(注意,对于虚读的定义是两次查询,包括不可重复读也是,但是很多介绍银行取款存款的例子中,只有一次查询),会导致用户无法确定该
用哪个结果,但是我们假设两次读取的数据都是真实的,那么以后面读取的数据为准,这样是符合事实逻辑的啊,
就是不明白这种情况为什么会有问题,在实际场景中也想不出这样的问题在哪里。思来想去,觉得在真实场景中因为这虚读或不重复读出现问题的可能就是一种很极端的情况:
在特定时刻下,需要对表中id<10的数据进行一次删除或者其他的一些处理吧,反正就是要对此刻以前在表中满足一定条件的数据进行处理。因此在事务1中,第一次查询获得了此刻,在表中满足条件的数据。然后这时,另一个事务添加了
一条也满足这个条件的数据(或者是对满足条件的数据进行了修改)。那么在继续进行事务1的处理(比如删除或者其他的一些操作),就会将事务2添加的数据(或者修改的数据)一并给处理了,但是,事务2添加的数据是在特定时刻以后才出现的,是不能
被处理的。当然这样的情况是确实有可能发生,但是为了避免这样的情况,相信在写sql语句的时候有很多办法来避免的。
在此隔离级别下要注意一种情况,就是:事务1有两个相同的查询,在刚执行完第一次查询后,事务2在这个时候对查询的数据进行了修改,并且提交了。那么事务1的第二个查询获取的数据就是新的数据。这一切看起来很正常,但是要注意的是,在事务1的两个
相同查询中获得了不同的数据,这种情况看似合理,但实则是每次读取到的值可能会有所不同,这种现象被称为不可重复读取或不一致解析,即既是虚读,又是不可重复读取。
更进一步则是在事务1中的第二次查询后,根据查询的结果来修改数据,并且成功提交了,这就是第二类更新丢失了。
REPEATABLE READ隔离级别
如果要解决虚读的问题,至少将隔离级别调到repeatable read。
看下面的试验,将隔离级别设置为repeatable read,在执行查询后,没有完成事务
接着在另一个窗口中执行下面的修改事务,会看到事务一直在等待执行。这时候将上面的事务如果还有第二次读取,那么读取的结果和第一次肯定是相同的,因为没有被其他事务修改。提交或回滚后,下面的事务就立即执行
通过这个试验可以看到, REPEATABLE READ隔离成功解决了不可重复读的问题,同时也解决了第二类更新丢失的问题。
但是这个隔离会出现死锁的情况,看下面的试验:
事务中先执行查询,再执行修改
可以看到,这里死锁了,一直在执行,即使先修改后查询也是一样。原因:在这种隔离下,查询的时候会获取一个共享锁,这个共享锁必须直到事务完成后才释放(前面两种隔离不会这样),然后遇到了修改语句,需要获取一个排它锁,但是排他锁和
共享锁不能同时存在,因此就一直等着,死锁了。(前面两种隔离,如果在同一个事务中,先有修改,再有查询,那么也会造成死锁)
还有一个问题,看下面的试验:
先执行事务:执行红框中的部分,表示这个事务只执行了一个查询,查询结果如下
然后第二个事务来了:这个能成功执行,因为第一个事务的共享锁是锁定了id为1到6的行,但是id=8的行没有锁住,所以可以获得这行的排它锁。(不知道这样理解对不对)
然后继续执行第一个事务的后面部分,查询结果
可以看到,同一个事务的两次同样的查询,获得了不同的结果,这就是虚读。
ERIALIZABLE隔离级别
为了防止幻影读取,需要将隔离级别提升为SERIALIZABLE,最重要的部分是SERIALIZABLE隔离级别的行为类似于REPEATABLE READ即它要求读取者获取一个共享锁来进行读取,并持有锁到事务结束,但是SERIALIZABLE隔离级别添加了另外一个方面-在逻辑上,该隔离级别要求读取者锁定查询筛选所限定的键的整个范围。这意味着读取者锁定的不仅是查询筛选限定的现有行,也包括将来行,或者准确地说,它会阻止其他事务尝试添加读取者查询筛选限定的行。
基于行版本的隔离级别
在SQL Server中存在两种基于行版本控制技术的隔离级别:SNAPSHOT、READ COMMITTED SNAPSHOT。将提交行之前的版本存储在tempdb中,SNAPSHOT隔离级别在逻辑上类似于SERIALIZABLE隔离级别,READ COMMITTED SNAPSHOT隔离级别类似于READ COMMITTED隔离级别,但是,读取者使用基于行版本控制的隔离级别并不不会发出共享锁,所以在请求的数据以排他锁锁定时它们不会等待,读取者仍旧会获得类似于SERIALIZABLE和READ COMMITTED的一致性级别,如果当前版本不是它们希望看到的版本,那么SQL Server会给读取者提供一个较旧的版本。
如果启用了任何基于快照的隔离级别,在修改tempdb之前,DELETE和UPDATE语句需要复制行的版本,对于INSERT语句则不需要再tempdb中版本化,因为它不存在早期的版本,但需要注意的是,启用任何基于行版本控制的隔离级别对于数据更新和删除的性能可能会有负面影响,由于它们不会获取共享锁,并且哎数据被以排他方式锁定或是数据版本不是所期望的版本时不需要等待,因此对于读取者的性能通常会有所改善。
SNAPSHOT隔离级别
要想在企业部署的SQL Server实例中允许事务以SNAPSHOT隔离级别工作,首先需要在查询窗口执行以下代码打开快照隔离级别。如下:tsql2012是数据库名
ALTER DATABASE TSQL2012 SET ALLOW_SNAPSHOT_ISOLATION ON
下面通过试验来看看SNAPSHOT的行为:
执行下面的 事务,事务未完成。这如果没有执行刚才的alert语句,这个事务是会死锁的,原因上面已经解释过了
然后另一个事务:
从查询结果看,获取的数据是第一个事务修改前的数据。然后现在将第一个事务提交,再来看看第二个事务的后半部分的查询结果
看到,只要第二个事务没有提交,那么它不管进行多少次查询,获得的结果都是一样的
SNAPSHOT隔离级别可以防止更新冲突,但不会像REPEATABLE READ和SERIALIZABLE隔离级别那样产生死锁,SNAPSHOT隔离级别的事务失败,表明检测到了更新冲突,SNAPSHOT隔离级别通过检查存储的版本来检测更新冲突,它可以发现在事务的读取和写入之间是否有另一个事务修改了数据。
READ_COMMITTED_SNAPSHOT隔离级别
该隔离级别也是基于行版本控制,它与SNAPSHOT隔离级别区别在于,读取者获得是【语句】启动时可用的最后提交的行版本,而不是【事务】启动时可用的最后提交的行版本,READ_COMMITTED_SNAPSHOT也不会检测更新冲突,导致类似于READ COMMITTED隔离级别,但在所请求资源以排他锁锁定时,不会请求共享锁并且不会等待。在企业内部部署的SQL Server中要想启动READ_COMMITTED_SNAPSHOT隔离级别,需要打开唯一会话来设置,否则无法进行启用(启用该隔离级别实际上是将READ COMMITTED隔离级别在语义上改变为READ_COMMITTED_SNAPSHOT隔离级别)。要启用该隔离级别,须执行如下语句:
ALTER DATABASE TEST SET SINGLE_USER WITH ROLLBACK IMMEDIATE --数据库置为单用户模式
ALTER DATABASE TEST SET READ_COMMITTED_SNAPSHOT ON
用这种隔离级别将事务处理完成后,记得要将数据库重置为多用户模式,并且关闭这种隔离
ALTER DATABASE TEST set MULTI_USER
ALTER DATABASE TEST SET READ_COMMITTED_SNAPSHOT OFF
这个在试验的时候,没法在同一个窗口打开两个不同的事务,所以没法验证了。