事务和锁

 

 

本文并不全是原创,我参考的原文在这里html


 背景:sql

       当用户并发尝试访问同一数据的时,SQL Server尝试用锁来隔离不一致的数据和使用隔离级别查询数据时控制一致性(数据该如何读取),提及锁就会联想到事务,事务是一个工做单元,包括查询/更新数据和数据定义。数据库

锁类型

在SQL Server中,根据资源的不一样,锁分为如下三种类型:
    行锁:是SQL Server中数据级别中粒度最小的锁级别,行锁根据表是否存在汇集索引,分为键值锁和标识锁
    页锁:针对某个数据页添加的锁,在T-SQL语句中,使用了页锁就不会在使用相同类型的行锁,反之依然,在对数据页加锁后,没法在对其添加不兼容的锁
    表锁:添加表锁则没法添加与其不兼容的页å锁和行锁session

锁模式

   共享锁(S):发生在数据查找以前,多个事务的共享锁之间能够共存
   排他锁(X):发生在数据更新以前,排他锁是一个独占锁,与其余锁都不兼容
   更新锁(U):发生在更新语句中,更新锁用来查找数据,当查找的数据不是要更新的数据时转化为S锁,当是要更新的数据时转化为X锁
   意向锁:发生在较低粒度级别的资源获取以前,表示对该资源下低粒度的资源添加对应的锁,意向锁有分为:意向共享锁(IS) ,意向排他锁(IX),意向更新锁(IU),共享意向排他锁(SIX),共享意向更新锁(SIU),更新意向排他锁(UIX)
   共享锁/排他锁/更新锁通常做用在较低级别上,例如数据行或数据页,意向锁通常做用在较高的级别上,例如数据表或数据。锁是有层级结构的,若在数据行上持有排他锁的时候,则会在所在的数据页上持有意向排他锁. 在一个事务中,可能因为锁持有的时间太长或个数太多,出于节约资源的考虑,会形成锁升级
   除了上述的锁以外,还有几个特殊类型的锁,例如架构锁,架构锁包含两种模式,架构稳定锁(Sch-S)和架构更新锁(Sch-M) ,架构稳定锁用来稳定架构,当查询表数据的时候,会对表添加架构稳定锁,防止架构发生改变。当执行DDL语句的时候,会使用架构更新锁,确保没有任何资源对表的占用。大数据量的表避免执行DDL操做,这样会形成架构更新锁长时间占用资源,影响其余操做,除非必要否则不要执行DDL语句,如在必要的状况下添加字段,须要先给字段初始化,在设置为非空。架构

锁的兼容性

如何查看一个事务中所请求的锁类型和锁的顺序,可以使用SQL Profiler 查看 Mode 属性并发

数据准备

IF OBJECT_ID('dbo.Nums','u') IS NOT NULL
    DROP TABLE dbo.Nums; GO
CREATE TABLE dbo.Nums ( ID INT PRIMARY KEY, NUM INT ); GO
IF EXISTS(SELECT * FROM SYS.SEQUENCES WHERE OBJECT_ID=OBJECT_ID('dbo.NumSequence')) DROP SEQUENCE dbo.NumSequence; GO
CREATE SEQUENCE dbo.NumSequence MINVALUE 1 MAXVALUE 1000 NO CYCLE GO
DECLARE @num AS INT = NEXT VALUE FOR dbo.NumSequence INSERT INTO dbo.Nums VALUES(@num,@num); GO 1

 

 

事务的隔离级别

事务

事务是一个工做单元,包含查询/修改数据以及修改数据定义的多个活动的组合,提及事务就须要提起事务的四个基本特性ACID:
   原子性:事务要么所有成功,要么所有失败。
   一致性:事务为提交前或者事务失败后,数据都和未开始事务以前一致
   隔离性:事务与事务之间互不干扰
   持久性:事务成功后会被永久保存起来,不会在被回滚app

隔离级别

事务的隔离级别控制并发用户的读取和写入的行为,即不一样的隔离界别对锁的控制方式不同,隔离级别主要分为两种类型:悲观并发控制和乐观并发控制,悲观并发控制有:READ UNCPOMMITTED / READ COMMITTED (会话默认) /REPEATABLE READ / SERIALIZABLE . 乐观并发控制主要以在Tempdb中建立快照的方式来实现,有:SNAPSHOT 和 READ COMMITTED SHAPSHOT,也被称为基于行版本的控制的隔离级别。性能

 

READ UNCOMMITTED

这个就至关于在查询语句的后面加上with(nolock),此隔离级别的主要特色是能够读取其余事务中未提交更改的数据(该语句 READ UNCOMMITTED是对整个批处理或SP不加锁,所以 后面全部的T-SQL查询语句都是不加S锁的),该隔离级别下请求查询的数据不须要共享锁(例如 tranA是一个delete TestTable操做,同时你查询TestTable的时候就须要先在TestTable的查询行上面加S锁,所以就要等待delete的操做结束,X锁释放掉,可是你使用 READ UNCOMMITTED以后,你在查询的时候就不加S锁了,因此就能访问放了X锁的语句),这样对于请求的行正在被更改,不会出现阻塞,这就形成了脏读.此隔离级别是最低的隔离级别,并发性良好,可是对于数据的一致性方面有缺陷,在一些不重要的查询中能够采用这种方式
以上面的表为例,开始两个会话,在会话1中运行以下代码:大数据

BEGIN TRAN
    UPDATE  dbo.Nums SET NUM = 10
    WHERE ID = 1

开启会话2而且运行以下代码:spa

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
GO
SELECT * FROM dbo.Nums WHERE ID = 1

查看运行结果

在事务未提交成状况下,却读取到了数据,这就是脏读。

另外一种情形:

--会话1
BEGIN TRAN
UPDATE dbo.Nums SET NUM+=1 WHERE ID = 1  
--会话2
SELECT * FROM dbo.Nums WHERE ID=1 

 你会发现会话2一直在等待,由于会话1在 ID = 1 的语句上面放了X锁,而且处于事务中,而且事务没有commit致使 ID = 1 的行一直被X锁锁住,而致使会话2的S锁没办法放到ID = 1  的行上,因此一直在等待会话1事务的提交。,做为对比,看下面的语句:

--会话1
BEGIN TRAN
SELECT * FROM dbo.Nums WHERE ID=1
--会话2
UPDATE dbo.Nums SET NUM+=1 WHERE ID = 1

 

你会发现 会话2能够执行成功,疑问:会话1在ID=1的行上面放了S锁,而且会话1的事务也没有结束,会话2要更新就必须放X锁在ID=1的行上面,可是X锁是互斥锁,不可能放的上去啊?其实 读取操做一完成,资源上的共享锁(S 锁)就会被当即释放(除非将事务隔离级别设置为可重复读或更高级别,或者在事务持续时间内用锁定提示保留共享锁(S 锁),而X锁则是要等到事务提交以后才会释放),即便该读取操做是在一个事务中,而且该事务未完成

READ COMMITTED

这个是系统默认的一种状况。一样的,这个语句做用的是整个批处理,即下面全部的TSQL语句都具有这一点。此隔离级别能够看做是对READ UMCOMMITTED 隔离级别的升级,解决带了脏读的问题,主要由于当你查询的时候须要先请求共享锁定,因为锁之间的兼容性,形成阻塞(当你查询的语句上面放了X锁的时候),可是该模式也会带来一个问题那就是不可重复读(不理解看下面的例子),在同一事务中的两个相同的查询 查出来的结果不一致,主要是由于该隔离级别对应共享锁并不会一直保持(也就是说两条连续的查询语句,每一条都会加锁,可是两条之间是没有锁的,所以update和delete语句会在两条之间执行,致使两条查询语句显示的结果不同),在两条查询语句之间是没有锁存在的,这样其余事务就是更新数据
以上面的表为例,开始两个会话,在会话1中运行以下代码:

BEGIN TRAN
    UPDATE  dbo.Nums SET NUM = 10
    WHERE ID = 1

 

 在会话2中运行以下代码,该会话会被阻塞

SET TRANSACTION ISOLATION LEVEL READ COMMITTED
GO
SELECT * FROM dbo.Nums WHERE ID = 1

 

 打开会话3运行以下语句,查看当前阻塞状态,链接信息,阻塞语句等其余信息

SELECT request_session_id,resource_type,resource_database_id,DB_NAME(resource_database_id) AS dbname, resource_associated_entity_id,request_mode,request_status FROM sys.dm_tran_locks

 

根据上面获得的session_id ,能够用下面的语句查询更具体的信息:

SELECT session_id,most_recent_session_id,connect_time,last_read,last_write, most_recent_sql_handle FROM sys.dm_exec_connections WHERE session_id IN (54,55)

 

能够具体的查看到执行语句,要想知道具体某个会话阻塞缘由,即正在等待哪一个会话的资源,运行以下语句

SELECT session_id,blocking_session_id,command,text,database_id,wait_type,wait_resource,wait_time FROM sys.dm_exec_requests cross apply sys.dm_exec_sql_text(sql_handle) WHERE blocking_session_id > 0
会话 session_id 正在等待会话 blocking_session_id

下面咱们来讲说不可重复读的问题,新建会话1运行以下代码

SET TRANSACTION ISOLATION LEVEL READ COMMITTED
GO
BEGIN TRAN
    SELECT * FROM dbo.Nums WHERE ID=1  --A语句
    WAITFOR DELAY '00:00:10'
    SELECT * FROM dbo.Nums WHERE ID=1  --B语句

新建会话2并运行以下代码

BEGIN TRAN
    UPDATE dbo.Nums SET NUM+=1 WHERE ID = 1  
    COMMIT TRAN

查看会话1的运行结果如图,从图中能够看出两次读取出来的数据不一致,这就是不可重复读,意思就是:会话1的A语句可以正常执行获得结果集,而后释放S锁,而后会话2就开始了,因为ID = 1

REPEATABLE READ

此隔离级别能够看做的是READ COMMITTED 的升级,该模式能够解决READ COMMITTED 的不可重复读的问题,主要是由于该级别下对共享锁的占用时间较长,会一直持续到事务的结束(也就是说两条连续的查询语句,每一条都会加锁,而且两条之间也是有锁的,而且锁一直持续到事务结束)。可是该模式也会存在一个叫作幻读的缺陷,幻读指的是在查找必定范围内的数据时,其余事务对该范围的数据进行INSERT操做,致使再次执行相同的查询语句,查询的结果可能多或者是和第一句不一致,形成幻读的缘由是由于被锁定的数据行是在第一次查询数据时肯定的,对将来的数据并无锁。此隔离级别不建议在更新频率较高的环境下使用,会形成性能不佳
以上面的表为例,打开两个会话,在会话1中运行下面的代码:

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
GO
BEGIN TRAN
    SELECT * FROM dbo.Nums WHERE ID=1
    WAITFOR DELAY '00:00:10'
    SELECT * FROM dbo.Nums WHERE ID=1
COMMIT TRAN

 

打开会话2而且运行以下代码

BEGIN TRAN
    UPDATE dbo.Nums SET NUM+=1 WHERE ID = 1  
COMMIT TRAN

结果以下:

运行过程当中能够发现UPDATE的DML会一直等待会话1中事务的提交,并不会形成不可重复读,下面来演示下幻读的问题,从新打开两个会话,在会话1中运行下面的代码:

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
GO
BEGIN TRAN
    SELECT * FROM dbo.Nums WAITFOR DELAY '00:00:10'
    SELECT * FROM dbo.Nums COMMIT TRAN

打开会话2运行以下代码:

BEGIN TRAN
    INSERT INTO dbo.Nums VALUES(2,2) COMMIT TRAN

运行结果:

 

因为会话2在会话1延时的10s内增长了一笔(不会被阻塞,由于insert不会放锁在表上面),致使两个相同的查询结果却不一致,这就是幻读,固然,这也是 

REPEATABLE READ和READ UNCOMMITTED共同的缺点。

SERIALIZABLE

此隔离级别能够看做是 REPEADTABLE READ 的升级,解决了幻读的问题,由于该模式下不只能够锁定第一次查询的数据行,还能够锁定将来知足条件的数据行,是一个区间锁的概念,该级别不会出现上述的问题,可是相对的代价就是一致性强牺牲了并发性
以上表为例,修改会话1的隔离级别为 SERIALIZABLE,代码以下:

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE

从结果能够看到会话2一直在等待会话1的完成,关于锁的请求类型和顺序请打开SQL Profiler 自行查看.

分看两种状况,

第一种:查询中指定了主键id,那么就只是锁定了键值,会话2不须要等待,能够直接执行

--会话1
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
BEGIN TRAN
    SELECT * FROM dbo.Nums where id=2
    WAITFOR DELAY '00:00:10'
    SELECT * FROM dbo.Nums where id=2
COMMIT TRAN

 

--会话2
INSERT
INTO dbo.Nums VALUES(14,5)

 

查询的条件不是主键,那么锁定的就是整个表:

--会话1
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
BEGIN TRAN
    SELECT * FROM dbo.Nums where num=2
    WAITFOR DELAY '00:00:10'
    SELECT * FROM dbo.Nums where num=2
COMMIT TRAN

 

--会话2
INSERT INTO dbo.Nums VALUES(12,3)

 

事实是会话2一直在等待会话1执行完以后才执行,可是会话2插入的也不在会话1的区域内

固然,若是会话2改为 delete和update语句状况和insert也是同样的

 

SNAPSHOT

当前隔离级别和接下来要介绍的隔离级别都是乐观并发控制的两种模式,又称行版本控制的隔离级别,在tempdb中存储事务未提交以前的数据行,使用基于行版本的控制隔离级别不会请求共享锁,对于查询数据的请求直接从快照读取,可是这种快照方式仍是很消耗性能的,尤为是对于更新或删除操做,仍然会出现阻塞. SNAPSHOT级别对快照的读取是以事务为单位的,同一个事务中的读取操做都会读取同一快照,不管其余事务是否更新了快照。在 READ COMMITTED 的隔离级别下仍是会从快照读取,可是其余模式就按照自己的控制方式进行控制,目标是源表,只有SNAPSHOT隔离级别能够检测冲突。
要使用该隔离级别须要在数据库中打开任意会话执行以下代码:

ALTER DATABASE TEST  SET ALLOW_SNAPSHOT_ISOLATION ON

 

以上面的表为例,打开两个会话,在会话1中运行以下代码:

BEGIN TRAN
    UPDATE dbo.Nums set NUM +=1
    WHERE ID = 1

 

打开会话2并运行以下代码:

SET TRANSACTION ISOLATION LEVEL SNAPSHOT GO
BEGIN TRAN
    SELECT * FROM dbo.Nums WHERE ID = 1

 

此时会话2并无被阻塞,而是返回了以前的版本,结果以下:

 切换会会话1运行 COMMIT TRAN ,紧接着继续在会话2中在执行一遍相同的查询,执行结果以下

发现与上次的结果相同,可是会话1明明已经提交了,为何仍是原来的数据呢,这是由于该模式的特色,要是想读取新的数据须要,须要提交本次事务,继续在会话2中运行以下代码:

COMMIT TRAN
BEGIN TRAN
    SELECT * FROM dbo.Nums WHERE ID = 1
COMMIT TRAN

结果如图所示:

 

下面看一个冲突检测的例子
从新打开两个会话,在会话1中运行以下代码:

SET TRANSACTION ISOLATION LEVEL SNAPSHOT GO
BEGIN TRAN
    SELECT * FROM dbo.Nums WHERE ID = 1

 

打开会话2运行以下代码:

BEGIN TRAN 
    UPDATE dbo.Nums SET NUM =10000
    WHERE ID =1 

回到会话1,继续运行以下代码:

UPDATE dbo.Nums SET NUM =100
    WHERE ID =1

 

此时会话1出现阻塞,能够经过执行以下语句:

SELECT session_id,blocking_session_id,command,text,database_id,wait_type,wait_resource,wait_time FROM sys.dm_exec_requests cross apply sys.dm_exec_sql_text(sql_handle) WHERE blocking_session_id > 0

 从图中能够看出竞争的资源是源表的数据行,并非快照的,这就说明对于UPDATE 或者是DELETE 最终的目标是源表,切换会话2 运行 COMMIT TRAN 发现会话1中出现了错误:

 

READ COMMITTED SNAPSHOT 模式对于冲突检测这一案例结果是不支持,会话1中的更新操做会成功,读者能够自行实验。

READ COMMITTED SNAPSHOT

同SNAPSHOT很像,但对于快照的读取是以语句为单位的,同一个事务中的查询数据的语句每次都读取快照的最新版
要使用该隔离级别须要在数据库中打开任意会话执行以下代码:

ALTER DATABASE TEST SET READ_COMMITTED_SNAPSHOT ON

 

以上表为例,打开2个会话,在会话1运行以下代码:

BEGIN TRAN 
    UPDATE dbo.Nums SET NUM +=1
    WHERE ID =1

 

打开会话2,并运行以下代码:

BEGIN TRAN 
    SELECT * FROM dbo.Nums WHERE ID =1

 

运行结果为:

是从快照中读取出来的,继续在会话1中运行 COMMIT TRAN ,以后在会话2中的当前事务中继续执行相同的查询,结果以下:

 

这就是以前所说的语句为单位的读取快照,在这里有一个颇有趣的现象就是,在会话2中并未设置隔离级别,这是由于默认状况下的隔离级别为 READ COMMITTED 因为运行了如上语句修改数据库标记,故,会话的默认的隔离级别变成了 READ COMMITTED SNAPSHOT,当显示修改成其余隔离级别是,则会按照修改后的隔离级别运行。若修改会话2的隔离级别为 READ UNCOMMITTED 时,并不会进行快照查询,仍然出现了脏读。

对于解决脏读/不可重复读/幻读等问题,能够经过升级隔离级别的方式解决问题。

 

死锁

提及锁的问题,那固然少不了谈起死锁这种现象,主要发生于两个或多个事务之间存在相互阻塞,形成死锁,在SQL Server 中会牺牲工做最少的事务,SQL Server 能够设置一个DEADLOCK_PRIORITY的会话选项设置事务的在发生死锁的状况下牺牲的顺序,值在-10~10之间,在发生死锁的状况下,会优先牺牲数值最低的事务,无论其作的工做有多么的重要,当存在平级的时候,将根据工做数量进行牺牲。
下面来演示一个死锁的例子,以上面的表为例,并建立一个Nums副本表取名CopyNums,并添加(1,1)记录,打开两个会话,在会话1中执行以下代码:

SET DEADLOCK_PRIORITY 0
BEGIN TRAN
    UPDATE dbo.Nums SET NUM=100
    WHERE ID = 1

打开会话2运行以下代码:

SET DEADLOCK_PRIORITY 1
BEGIN TRAN
    UPDATE  dbo.CopyNums SET NUM = 100
    WHERE ID = 1

切换回会话1 继续运行以下代码:

SELECT * FROM dbo.CopyNums WHERE ID = 1

此时会发生阻塞,等待排他锁(X)释放,切换会话2运行以下代码:

SELECT * FROM dbo.Nums WHERE ID = 1

这次也会发生阻塞,可是阻塞一会你就会发现,会话1终止了,并出现以下错误:

为何会终止的是会话1呢?能够发如今会话中咱们设置了 DEADLOCK_PRIORITY,会牺牲数值低的那个会话事务,查看SQL Profiler 能够发现,确实有死锁现象发生(为了清晰仅显示死锁)

那么既然死锁会发生,就要有对应的避免死锁的对策:   1. 事务时间越长,保持锁的时间就越长,形成死锁的可能性就越大,检查事务中是否放置了过多的不该该属于同一工做单元的逻辑,有的话请移除到,从而缩短事务的时间   2. 上述死锁发生的关键在于访问顺序的问题,将两个会话中的语句变成一个顺序(都先操做Nums 或者 CopyNums ),就没有了死锁现象,因此在没有逻辑的单元中,调换顺序也会减小死锁的发生   3. 考虑选择隔离级别,不一样隔离级别对锁的控制方式不同,例如:行版本控制就不会请求共享锁(S)

相关文章
相关标签/搜索