SQLServer事务的隔离级别
事务的隔离级别
SQL Server通过在锁资源上使用不同类型的锁来隔离事务。为了开发安全的事务,定义事务内容以及应在何种情况下回滚至关重要,定义如何以及在多长时间内在事务中保持锁定也同等重要。这由隔离级别决定。应用不同的隔离级别,SQL Server赋予开发者一种能力,让他们为每一个单独事务定义与其他事务的隔离程度。事务隔离级别的定义如下:
ANSI 99定义了4种事务隔离级别,SQL Server 2005能够完全支持这些级别:
此外,SQL Server还有两种使用行版本控制来读取数据的事务级别(本章后文将详细检验这些隔离级别)。行版本控制允许一个事务在数据排他锁定后读取数据的最后提交版本。由于不必等待到锁释放就可进行读操作,因此查询性能得以大大增强。这两种隔离级别如下:
无论定义什么隔离级别,对数据的更改总是通过排他锁来锁定并直到事务结束时才释放。
很多情况下,定义正确的隔离级别并不是一个简单的决定。作为一种通用的规则,要选择在尽可能短的时间内锁住最少数据,但同时依然可以为事务提供它所需的安全程度的隔离级别。
已提交读
在SQL Server 2005中,
已提交读隔离级别是建立连接时的默认隔离级别。这个级别存在两种类型:已提交读和已提交读快照隔离级别。应用哪种类型由数据库选项定义。已提交读级别会在
读数据之前等待,直到阻塞锁被释放。已提交读快照级别会在数据被其他事务阻塞时使用行版本控制来读数据最后一次提交的版本。
使用已提交读级别:
BEGIN TRAN
SELECT
FirstName, LastName, EmailAddress
FROM
Person.Contact
WHERE
ContactID = 1
现在假设另一事务在事务打开状态下更改了EmailAddress。打开第二个查询窗口并执行以下批来UPDATE EmailAddress,但不提交事务:
USE AdventureWorks;
BEGIN TRAN
UPDATE
Person.Contact
SET
WHERE
ContactID = 1
这个UPDATE 语句会正常运行。一行受到了影响,即使数据在这个事务还没有运行完之前已被查询窗口1中的事务读取。因为已提交读级别并不会在事务结束前保持用于SELECT语句的共享锁。共享锁会在数据读取之后立即被SQL Server释放。需要一致读的时候这将是一个问题。我们将下面的"获取一致的可重复读操作"实现。
现在切换到查询窗口1并尝试再次读数据:
SELECT
FirstName, LastName, EmailAddress
FROM
Person.Contact
WHERE
ContactID = 1
由于SELECT语句被阻塞,因此这个查询并没有结束。SQL Server会尝试在ContactID= 1的键上获取一个共享锁,但是由于在查询窗口2中的UPDATE语句对其有一个排他锁,因此这个操作不可能完成。虽然查询窗口2处于已提交读级别(由于您没有更改默认级别),但排他锁依然存在。这个阻塞将持续存在,因为数据更改的排他锁会一直保持直到事务结束。
切换到查询窗口2,让查询窗口1中的查询继续运行。键入并执行以下SELECT语句检查数据库中的授权和等待的锁。
可以看一个状态为WAIT的共享锁。这是查询窗口1中运行的查询。它在等待查询窗口2中的查询,后者在同样的资源上有一个排他锁。
在查询窗口2中执行一个ROLLBACK TRAN语句来回滚UPDATE语句。然后切换回查询窗口1。可以看到,查询窗口1中的查询完成了,并且其结果与以前的一样。查询窗口2中的事务结束的时候,锁被释放了,以至查询窗口1中的查询不再被阻塞。由于查询窗口2中的事务回滚,因此查询窗口1中得到的结果是原来的数据。如果查询窗口2中的事务被提交,则查询窗口1中会得到新的数据作为结果。
在查询窗口1中执行一个COMMIT TRAN语句并关闭所有的查询窗口。
可以看出,在(默认)已提交读级别中SQL Server会
等到排他锁释放之后再进行读操作,以此来获取真正的提交数据。还可以看出,共享锁会持续到数据被读取之后,而排他锁会持续到事务提交之后。在许多事务几乎
同时更改数据的时候这种行为可能会造成问题。在这些情况下,由于排他锁造成的阻塞,读数据会非常慢。但在有些情况下,使用最后提交的数据版本是恰当的。在
这些情况下,可以将已提交读级别更改为已提交读快照级别。
如果要在窗口1读取数据的话,可以使用这样的方法:
SELECT
FirstName, LastName, EmailAddress
FROM
Person.Contact WITH (NOLOCK)
WHERE
ContactID = 1
让它取消所有的锁机制,那么排他锁也不会影响到这句查询。
使用NOLOCK注意:在 SQL Server 中,NOLOCK 提示将启用"未提交读"行为。在 SQL Server Mobile 中,使用 NOLOCK 提示仍会赋予"提交读"隔离级别。SQL Server Mobile 将维护数据副本,以确保可以读取数据而不需要使用共享锁帮助保护数据。
使用已提交读快照级别
激活已提交读快照级别
USE master;
ALTER DATABASE AdventureWorks
SET READ_COMMITTED_SNAPSHOT ON
注意:设置 READ_COMMITTED_SNAPSHOT 选项时,数据库中仅允许存在执行 ALTER DATABASE 命令的连接。在 ALTER DATABASE 完成之前,数据库中不允许有其他打开的连接。数据库不必处于单用户模式。
现在,执行以下代码开始一个事务并像前面一样更改EmailAddress(但要让事务处于打开状态):
USE AdventureWorks;
BEGIN TRAN
UPDATE Person.Contact
WHERE ContactID = 1;
打开第二个查询窗口并执行以下语句来读取ContactID 1的列Name和EmailAddress列。
USE AdventureWorks;
BEGIN TRAN
SELECT FirstName, LastName, EmailAddress
FROM Person.Contact
WHERE ContactID = 1;
返回了联系人Gustavo Achong的EmailAddress gustavo0@adventure-works.com,这是这一行最后提交的版本。不像没有快照的已提交读级别那样,这个查询不会被阻塞。关闭查询窗口2并切换到查询窗口1。
执行以下语句来回滚事务并切换回已提交读级别(这个查询将等待直到关闭查询窗口2):
ROLLBACK TRAN
GO
USE master;
ALTER DATABASE AdventureWorks
SET READ_COMMITTED_SNAPSHOT OFF
重要提示 这个隔离级别可以用于减少阻塞。但要意识到这是一个数据库选项。当它发生了更改,将在数据库系统中使用已提交读级别的所有事务也会改变它们的行为。因此,只有在所有这些事务读最后提交的数据版本与读真正提交的数据版本在逻辑上同样正确的时候,使用这种级别才是明智的。
获取一致的可重复读操作
已提交读级别的一个缺点是,一个事务读取的数据在事务运行期间可能被另一个事务更改。因此,在两种已提交读级别下,不能保证一致性读。获取一致性读的意思是,在一个事务中,读取的数据始终是一样的。
1. 已提交读在读数据的时候使用共享锁,但在读操作完成后会立即释放这个锁。因此,其他事务可以更改刚被读过的数据。
2. 已提交读快照读取最后一次提交的数据版本。当它第二次读数据的时候,最后一次提交的版本可能由于第二个事务已经提交了对数据的更改而变成一个新版本。
在需要一致性读的时候(例如对于报表),
可能这种不一致性会导致问题。想象一下,您的事务通过数据计算了一些商业数值。在已提交读级别中进行这种计算的时候,可能由于基础数据在事务计算过程中发
生了变化而导致这些值被错误计算。为了成功地执行这个计算,可以使用快照隔离级别。它会使用行版本管理来提供数据的提交版本,但与已提交读快照不同的是,
它总会提供在开始事务时最后提交的数据版本。因此,SQL Server始终会在整个事务执行过程中获取同样的数据。
使用快照隔离级别
快照隔离级别需要在数据库中一次性地激活。激活之后,每个连接可以在需要的时候使用它。
USE master;
ALTER DATABASE AdventureWorks
SET ALLOW_SNAPSHOT_ISOLATION ON;
现在假设我们希望运行一些基于Sales.SalesOrderDetail表的报表,但需要一致性的读操作。执行以下语句为事务激活快照隔离级别并开始一个返回订单行合计的事务。记住OrderTotal的值。
USE AdventureWorks;
SET TRANSACTION ISOLATION LEVEL SNAPSHOT
BEGIN TRAN
SELECT SUM(LineTotal) as OrderTotal
FROM Sales |

