事务隔离级别与阻塞

本篇文章参考《Microsoft SQL Server企业级平台管理实践》中第9章和第10章 阻塞与死锁

1、事务隔离级别

SQL Server数据库引擎支持下列隔离级别

1.1、未提交读

指定语句可以读取已由其他事务修改但尚未提交的行。也就是说,允许脏读。
未提交读的意思也就是,读的时候不申请共享锁。所以它不会被其他人的排他锁阻塞,它也不会阻塞别人申请排他锁。
SELECT * FROM TABLE WITH(NOLOCK)

1.2、已提交读

防脏读,指定语句不能读取已由其他事务修改但尚未提交的数据。这样可以避免脏读。其他事务可以在当前事务的各个语句之间更改数据,从而产生不可重复读取数据和幻像数据。

1.3、可重复读

防重复读,指定语句不能读取已由其他事务修改但尚未提交的数据,并且指定,其他任何事务都不能在当前事务完成之前修改由当前事务读取的数据。
对事务中的每个语句所读取的全部数据都设置了共享锁,并且该共享锁一直保持到事务完成为止。

1.4、可序列化

防幻影,语句不能读取已由其他事务修改但尚未提交的数据
任何其他事务都不能在当前事务完成之前修改由当前事务读取的数据
在当前事务完成之前,其他事务不能使用当前事务中任何语句读取的键值插入新行
SELECT * FROM TABLE WITH(HOLDLOCK)

--隔离级别
DBCC USEROPTIONS
GO
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
GO
SET TRANSACTION ISOLATION LEVEL READ COMMITTED
GO
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
GO
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
GO
SET TRANSACTION ISOLATION LEVEL SNAPSHOT
GO
View Code

2、共享锁持有情况

A、未提交读
在此示例中,一个read uncommitted事务将读取数据,然后由另一事务修改此数据。执行完的读操作不阻塞由其他事务执行的更新操作(读操作不申请共享锁)。同时,在其他事务已经做了更新操作后,读操作不会被阻塞,读取的是更新操作后的值,其他事务可继续更新、提交、回滚之前的操作。意味着读取操作有脏读。
在会话1上:

USE AdventureWorks2008;
GO
--修改事务隔离级别为未提交读
set transaction isolation level read uncommitted
GO
BEGIN TRANSACTION;
    -- 查询1
    -- 这个查询将返回员工有48小时休假时间.
    SELECT BusinessEntityID, VacationHours
        FROM HumanResources.Employee
        WHERE BusinessEntityID = 4;

在会话2上:

USE AdventureWorks2008;
GO
BEGIN TRANSACTION;
    -- 修改1
    -- 休假时间减8
    -- 修改不会被阻塞,因为会话1不会申请S锁
    UPDATE HumanResources.Employee
        SET VacationHours = VacationHours - 8
        WHERE BusinessEntityID = 4;

    -- 查询1
    -- 现在休假时间只有40小时
    SELECT VacationHours
        FROM HumanResources.Employee
        WHERE BusinessEntityID = 4;

在会话1上:

    -- 重新运行查询语句,不会被会话2阻塞,返回员工休假时间40小时
    -- 查询2
    SELECT BusinessEntityID, VacationHours
        FROM HumanResources.Employee
        WHERE BusinessEntityID = 4;

在会话2上:

-- 回滚事务
ROLLBACK TRANSACTION;
GO

在会话1上:

    -- 查询3
    -- 这里返回48,因为会话2回滚了事务
    SELECT BusinessEntityID, VacationHours
        FROM HumanResources.Employee
        WHERE BusinessEntityID = 4;

-- 回滚事务
ROLLBACK TRANSACTION;
GO

B、已提交读
在此示例中,一个read committed事务将读取数据,然后由另一事务修改此数据。执行完的读操作不阻塞由其他事务执行的更新操作(当前语句做完释放共享锁)。但是,在其他事务已经做了更新操作后,读操作会被阻塞住,直到更新操作事务提交/回滚为止。
在会话1上:

USE AdventureWorks2008;
GO
--修改事务隔离级别为已提交读
set transaction isolation level read committed
GO
BEGIN TRANSACTION;
    -- 查询1
    -- 这个查询将返回员工有48小时休假时间.
    SELECT BusinessEntityID, VacationHours
        FROM HumanResources.Employee
        WHERE BusinessEntityID = 4;

在会话2上:

USE AdventureWorks2008;
GO
BEGIN TRANSACTION;
    -- 修改1
    -- 休假时间减8
    -- 修改不会被阻塞,因为会话1不会持有S锁不放
    UPDATE HumanResources.Employee
        SET VacationHours = VacationHours - 8
        WHERE BusinessEntityID = 4;

    -- 查询1
    -- 现在休假时间只有40小时
    SELECT VacationHours
        FROM HumanResources.Employee
        WHERE BusinessEntityID = 4;

在会话1上:

    -- 重新运行查询语句,会被会话2阻塞
    -- 查询2
    SELECT BusinessEntityID, VacationHours
        FROM HumanResources.Employee
        WHERE BusinessEntityID = 4;

在会话2上:

-- 提交事务
COMMIT TRANSACTION;
GO

在会话1上:

    -- 此时先前被阻塞的查询结束,返回会话2修改好的新数据:40
    -- 查询3
    -- 这里返回40,因为会话2已经提交了事务
    SELECT BusinessEntityID, VacationHours
        FROM HumanResources.Employee
        WHERE BusinessEntityID = 4;

    -- 修改2
    -- 这里会成功.
    UPDATE HumanResources.Employee
        SET SickLeaveHours = SickLeaveHours - 8
        WHERE BusinessEntityID = 4;

-- 可以回滚会话1的修改
-- 会话2的修改不会受影响
ROLLBACK TRANSACTION;
GO

C、可重复读
在此示例中,一个repeatable read事务将读取数据,然后由另一事务修改此数据。执行完的读操作会阻塞由其他事务执行的更新操作(共享锁保持到当前事务完成)。同时,在其他事务已经做了更新操作后,读操作会被阻塞住,直到更新操作事务提交/回滚为止。
在会话1上(操作前已将时间更新为48):

USE AdventureWorks2008;
GO
--修改事务隔离级别为已提交读
set transaction isolation level repeatable read
GO
BEGIN TRANSACTION;
    -- 查询1
    -- 这个查询将返回员工有48小时休假时间.
    SELECT BusinessEntityID, VacationHours
        FROM HumanResources.Employee
        WHERE BusinessEntityID = 4;

在会话2上:

USE AdventureWorks2008;
GO
BEGIN TRANSACTION;
    -- 修改1
    -- 休假时间减8
    -- 修改会被阻塞,因为会话1持有S锁到事务完成
    UPDATE HumanResources.Employee
        SET VacationHours = VacationHours - 8
        WHERE BusinessEntityID = 4;

在会话1上:

-- 提交事务
COMMIT TRANSACTION;
GO

在会话2上:

    -- 此时先前被会话1阻塞的更新完成
    -- 查询1
    -- 现在休假时间只有40小时
    SELECT VacationHours
        FROM HumanResources.Employee
        WHERE BusinessEntityID = 4;

在会话1上(会话1依旧是可重复读,只是以下语句没有显示开启事务):

    -- 重新运行查询语句,会被会话2阻塞
    -- 查询2
    SELECT BusinessEntityID, VacationHours
        FROM HumanResources.Employee
        WHERE BusinessEntityID = 4;

在会话2上:

-- 回滚事务
ROLLBACK TRANSACTION;
GO

会话2一旦回滚(提交)事务,被阻塞的会话1将返回结果48(40)
事务隔离级别越高,共享锁被持有的时间越长。而可序列化还要申请粒度更高的范围锁,并一直持有到事务结束。所以,如果阻塞发生在共享锁上面,可以通过降低事务隔离级别得到缓解
SQLServer在处理排他锁的时候,4个事务隔离级别都是一样的。都是在修改的时候申请,直到事务提交的时候释放(而不是语句结束以后立即释放)。如果阻塞发生在排他锁上面,是不能通过降低事务隔离级别得到缓解的。

3、如何监视锁的申请、持有和释放

在已提交读隔离级别下,会话1修改但尚未提交,会话2读取数据
通常可以使用sp_lock命令来列出当前SQLServer里所有的连接持有的锁的内容

在SQLServer 2005以后,这个功能可以由直接查询sys.dm_tran_locks这张系统动态管理视图来实现

SELECT request_session_id
      ,resource_type
      ,resource_associated_entity_id
      ,request_status
      ,request_mode
      ,resource_description
FROM   sys.dm_tran_locks
ORDER BY
       request_session_id
View Code


当然也可以结合其他DMV,直接查出某个数据库上面的锁是在哪些表格,以及哪些索引上面

USE AdventureWorks2008
GO
SELECT request_session_id
      ,resource_type
      ,resource_associated_entity_id
      ,request_status
      ,request_mode
      ,resource_description
      ,p.object_id
      ,OBJECT_NAME(p.object_id) AS OBJECT_NAME
      ,p.*
FROM   sys.dm_tran_locks
       LEFT JOIN sys.partitions p
            ON  sys.dm_tran_locks.resource_associated_entity_id = p.hobt_id
WHERE  resource_database_id = DB_ID('AdventureWorks2008')
ORDER BY
       request_session_id
      ,resource_type
      ,resource_associated_entity_id
View Code


可以借助Profiler定义一个跟踪,添加Lock:Accquired,Lock:Released事件,监视语句执行过程中SQLServer对锁的申请和释放。

3.1、定位阻塞

(1)SQLServer里有没有阻塞发生?是什么时候发生的?在哪个数据库上?

SELECT spid,kpid,blocked,waittime,s.waitresource,dbid,s.last_batch,status FROM sys.sysprocesses s WHERE spid>50

重点查看blocked<>0的连接,如果不为0,而且也不是-2、-3、-4,那它就是被SPID等于这个字段值的那个连接给阻塞住了。一般来讲,阻塞源头的blocked字段会是Null,如果它也不等于0,说明它也被别人阻塞住了,要继续查找阻塞住它的连接。
(2)和阻塞有关的连接是从哪些客户应用来的?

SELECT spid,kpid,blocked,hostname,program_name,hostprocess,loginame,nt_domain,nt_username,net_address,net_library FROM sys.sysprocesses s WHERE spid>50

(3)为什么阻塞会发生?这个问题包括:
a、现在阻塞发生在哪个或哪些资源上?
sp_lock寻找状态为wait的锁资源,或者直接运行下面查询

USE AdventureWorks2008
GO
SELECT request_session_id
      ,resource_type
      ,request_status
      ,request_mode
      ,resource_description
      ,OBJECT_NAME(p.object_id) AS OBJECT_NAME
      ,i.name index_name
FROM   sys.dm_tran_locks
       LEFT JOIN sys.partitions p
            ON  sys.dm_tran_locks.resource_associated_entity_id = p.hobt_id
       LEFT JOIN sys.indexes i
            ON  p.object_id = i.[object_id]
                AND p.index_id = i.index_id
ORDER BY request_session_id,resource_type
View Code

b、阻塞的源头是在做什么事情的时候申请了这些锁?为什么会申请这些锁?
锁可能是会话正在运行中的语句申请的,但也可能是这个会话在先前开启了一个事务,一直都没有提交或回滚,锁资源是事务开启后的任何一个语句申请的,当时阻塞可能还没有发生。
如果是前者,只须抓住连接发过来的最后一句话即可。如果是后者,则要在阻塞发生之前预先开启SQL Trace,一直跟踪到问题发生。如果阻塞问题已经发生而跟踪没有开启,那就没有办法知道事务是怎么开启的,以及锁是什么语句申请的了。所以有时候要抓住阻塞问题的根源,必须下决心在出问题之前就开启服务器端跟踪,等到问题重现为止。
可以运行一些脚本得到某个连接当前正在运行的语句,和空闲连接(sleeping)上次运行的最后一个批处理语句。
下面这个查询可以返回所有正在运行中的连接和它正在运行的语句。如果一个连接处于空闲状态,那就不会被返回。

SELECT p.session_id
      ,p.request_id
      ,p.start_time
      ,p.status
      ,p.command
      ,p.blocking_session_id
      ,p.wait_type
      ,p.Wait_time
      ,p.wait_resource
      ,p.total_elapsed_time
      ,p.open_transaction_count
      ,p.transaction_isolation_level
      ,SUBSTRING(
           qt.text
          ,p.statement_start_offset/2
          ,(
               CASE WHEN p.statement_end_offset=-1 THEN LEN(CONVERT(NVARCHAR(MAX) ,qt.text))*2
                    ELSE p.statement_end_offset
               END-p.statement_start_offset
           )/2
       ) AS "SQL statement"
      ,p.statement_start_offset
      ,p.statement_end_offset
      ,batch = qt.text
FROM   MASTER.sys.dm_exec_requests p
       CROSS APPLY sys.dm_exec_sql_text(p.sql_handle) AS qt
WHERE  p.session_id>50
View Code

运行DBCC INPUTBUFFER(spid)可以获得从客户端发送到SQLServer实例的最后一个批处理语句。这句话的优点是不管连接是否正在运行,都会返回结果。缺点是它返回的是整个批处理语句,而不是当前正在执行的子句。
找到以后,一般就能确定阻塞是否是当前运行的语句造成的。如果阻塞发生在表A上,而当前这句话不可能在这个表上加相应的锁,那基本上可以断定阻塞是由于一个先前开启的事务导致的。
c、阻塞的源头当前的状态是什么?是一直在执行,还是已经进入空闲状态?

SELECT spid,kpid,blocked,waittype,status FROM sys.sysprocesses s WHERE spid>50

如果kpid和waittype两个字段都是0,就是一个处于空闲状态的连接。
d、如果它一直在执行,为什么要执行这么久?
如果一个连接的kpid<>0(连接拿到来了一个线程资源),waittype=0(它不需要等待任何资源),它的状态就会是runnable或running。如果一个连接的kpid<>0 and waittype<>0,则说明它要等待某个资源才能继续执行。这时候连接状态一般会是suspended。
e、如果已经进入空闲状态,那为什么没有释放锁资源?

SELECT spid,kpid,blocked,waittype,status,open_tran FROM sys.sysprocesses s WHERE spid>50

如果一个连接的kpid=0(连接没有占用线程资源) and waittype=0(它不需要等待任何资源),那么这个连接已经完成了客户端发过来的所有请求,现在进入了空闲状态,正在等待客户端发送新的请求。
按道理在这种情况下连接应该释放先前申请的锁资源才对。如果这时它还是阻塞的源头,一般是因为它有先前开启的事务没有及时提交。这可以通过检查sysprocesses里的open_tran>0确认。
f、其他被阻塞的连接它们想要做什么?为什么也要申请这些锁资源?
使用步骤b里的脚本,也能够知道被阻塞住的连接正在运行的语句。然后再去比较sp_lock的结果,就能大致判断它申请的锁数量是否合理。如果不是很合理,可以通过优化语句、加合适的索引解决。

3.2、举个例子(已提交读)

会话1(spid=54)开启事务更新数据尚未提交

USE AdventureWorks2008;
GO
BEGIN TRANSACTION;
    -- 修改1
    -- 休假时间减8
    UPDATE HumanResources.Employee
        SET VacationHours = VacationHours - 8
        WHERE BusinessEntityID = 4;

会话2(spid=55)读取会话1中修改的行

USE AdventureWorks2008;
GO
BEGIN TRANSACTION;
    -- 查询1
    -- 这个查询会被会话1阻塞
    SELECT BusinessEntityID, VacationHours
        FROM HumanResources.Employee
        WHERE BusinessEntityID = 4;

在已提交读的隔离级别下,指定语句不能读取已由其他事务修改但尚未提交的数据。
此时SQLServer里阻塞情况

SPID 55的blocked字段不为0,而是54。SPID 54的相应字段为0。可以得出结论,此时有阻塞发生,55被54阻塞住了。
和阻塞有关的连接是从哪些应用来的

发生阻塞和阻塞别人的连接分别是SPID 55和SPID 54,它们都是来自WORK上的Management Studio。
现在阻塞发生在哪个或哪些资源上

SPID 55等待表Employee的主键PK_Employee_BusinessEntityID的S锁,而SPID 54在表Employee的主键PK_Employee_BusinessEntityID持有X锁。
阻塞的源头是在做什么事情的时候申请了这些锁?为什么会申请这些锁?

由于SPID 54已经是空闲连接,因此只能用DBCC INPUTBUFFER(spid)返回最后一个批处理语句。返回的语句是更新Employee表,因此它在对应键上申请X锁。也就是说阻塞是由这个UPDATE语句造成的。注意,SPID 54中也可以由UPDATE...GO...SELECT...此时,DBCC INPUTBUFFER(spid)仅会返回SELECT部分,显然SELECT只会加S锁,它和SPID 55相互兼容,那基本上可以断定阻塞是由于一个先前开启的事务导致的。
阻塞的源头当前的状态是什么?是一直在执行,还是已经进入空闲状态?如果已经进入空闲状态,那为什么没有释放锁资源?

SPID 54的kpid和waittype两个字段都是0,就是一个处于空闲状态的连接。open_tran=1,说明有一个未提交的事务。
其他被阻塞的连接它们想要做什么?为什么也要申请这些锁资源?

被阻塞的SPID 55从Employee表读取被SPID 54修改的行,需申请S锁,无法通过优化语句、添加索引解决。只能在阻塞源头上,让其更新完成后及时提交事务。

posted @ 2016-03-10 21:44  Uest  阅读(1711)  评论(0编辑  收藏  举报