SQL Server事务的隔离级别和锁
背景
当用户并发尝试访问同一数据的时,SQL Server尝试用锁来隔离不一致的数据和使用隔离级别查询数据时控制一致性(数据该如何读取),说起锁就会联想到事务,事务是一个工作单元,包括查询/更新数据和数据定义。
锁
锁类型
在SQL Server中,根据资源的不同,锁分为以下三种类型:
行锁:是SQL Server中数据级别中粒度最小的锁级别,行锁根据表是否存在聚集索引,分为键值锁和标识锁
页锁:针对某个数据页添加的锁,在T-SQL语句中,使用了页锁就不会在使用相同类型的行锁,反之依然,在对数据页加锁后,无法在对其添加不兼容的锁
表锁:添加表锁则无法添加与其不兼容的页å锁和行锁
锁模式
共享锁(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
运行UPDATE dbo.Nums SET Num += 1
查看SQL Profiler 的跟踪,可以清楚的看到锁的请求顺序和类型(请自定配置跟踪模版,以便于想要看到自己想要的属性)


事务的隔离级别
事务
事务是一个工作单元,包含查询/修改数据以及修改数据定义的多个活动的组合,说起事务就需要提起事务的四个基本特性ACID:
原子性:事务要么全部成功,要么全部失败。
一致性:事务为提交前或者事务失败后,数据都和未开始事务之前一致
隔离性:事务与事务之间互不干扰
持久性:事务成功后会被永久保存起来,不会在被回滚
隔离级别
事务的隔离级别控制并发用户的读取和写入的行为,即不同的隔离界别对锁的控制方式不一样,隔离级别主要分为两种类型:悲观并发控制和乐观并发控制,悲观并发控制有:READ UNCPOMMITTED / READ COMMITTED (会话默认) /REPEATABLE READ / SERIALIZABLE . 乐观并发控制主要以在Tempdb中创建快照的方式来实现,有:SNAPSHOT 和 READ COMMITTED SHAPSHOT,也被称为基于行版本的控制的隔离级别。
READ UNCOMMITTED
此隔离级别的主要特点是可以读取其他事务中未提交更改的数据,该隔离级别下请求查询的数据不需要共享锁,这样对于请求的行正在被更改,不会出现阻塞,这就造成了脏读.此隔离级别是最低的隔离级别,并发性良好,但是对于数据的一致性方面有缺陷,在一些不重要的查询中可以采用这种方式
以上面的表为例,开始两个会话,在会话1中运行如下代码:
BEGIN TRAN
UPDATE dbo.Nums SET NUM = 10
WHERE ID = 1
开启会话2并且运行如下代码:
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
GO
SELECT * FROM dbo.Nums WHERE ID = 1
查看运行结果

在事务未提交成情况下,却读取到了数据,这就是脏读,可以通过SQL Profiler 查看具体的请求锁的类型和顺序。


如图可以看出,对于会话2只请求了架构稳定锁(Sch-S) 并未请求共享锁
READ COMMITTED
此隔离级别可以看作是对READ UMCOMMITTED 隔离级别的升级,解决带了脏读的问题,主要方式是对应查询数据的请求需要先请求共享锁定,由于锁之间的兼容性,造成阻塞,但是该模式也会带来一个问题那就是不可重复读,在同一事务中的两个相同的查询 查出来的结果不一致,主要是因为该隔离级别对应共享锁并不会一致保持,在两条查询语句之间是没有锁存在的,这样其他事务就是更新数据
以上面的表为例,开始两个会话,在会话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
运行结果如图:

从图中可以看出当前,会话55的请求状态为WAIT,也就是阻塞状态,图中54为UPDATE操作的DML正在持有一个更新锁(X).进一步查看进程的相关信息,运行如下代码
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)

可以看到各个进程的连接时间,最后一次读取时间和最后一次写入时间,和对应的T-SQL语句,要想查看具体的语句信息请运行如下代码
SELECT session_id,text FROM sys.dm_exec_connections CROSS APPLY sys.dm_exec_sql_text(most_recent_sql_handle) AS A 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

从图中可以看出,会话55正在等待会话54及竞争的资源信息,等待类型和等待时间,从上述的语句可以轻松查看想要知道的信息,对于各个会话对锁的请求顺序和类型请自行查看SQL Profiler.
下面我们来说说不可重复读的问题,新建会话1运行如下代码
SET TRANSACTION ISOLATION LEVEL READ COMMITTED
GO
BEGIN TRAN
SELECT * FROM dbo.Nums WHERE ID=1
WAITFOR DELAY '00:00:10'
SELECT * FROM dbo.Nums WHERE ID=1
新建会话2并运行如下代码
BEGIN TRAN
UPDATE dbo.Nums SET NUM+=1 WHERE ID = 1
COMMIT TRAN
查看会话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
打开会话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的运行结果可以看到,读取出了2行数据,被称为幻读,关于锁的请求类型和顺序请打开SQL Profiler 自行查看.
SERIALIZABLE
此隔离级别可以看作是 REPEADTABLE READ 的升级,解决了幻读的问题,因为该模式下不仅可以锁定第一次查询的数据行,还可以锁定未来满足条件的数据行,是一个区间锁的概念,该级别不会出现上述的问题,但是相对的代价就是一致性强牺牲了并发性
以上表为例,修改会话1的隔离级别为 SERIALIZABLE,代码如下:
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
从结果可以看到会话2一直在等待会话1的完成,关于锁的请求类型和顺序请打开SQL Profiler 自行查看.
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)
SQL Server事务的隔离级别和锁的更多相关文章
- SQL Server事务的隔离级别
SQL Server事务的隔离级别 ########## 数据库中数据的一致性 ########## 针对并发事务出现的数据不一致性,提出了4个级别的解决方法: 隔离级别 第一类丢失更新 脏读 ...
- SQL Server事务、隔离级别详解(二十九)
前言 事务一直以来是我最薄弱的环节,也是我打算重新学习SQL Server的出发点,关于SQL Server中事务将分为几节来进行阐述,Always to review the basics. 事务简 ...
- SQL Server 事务与隔离级别实例讲解
上班途中,你在一处ATM机前停了下来.正当你在敲入密码的时候,你的一位家人也正在镇上的另一处TAM机上输入密码.你打算从某个还有500元余额的账户上转出400元,而你的家人想从同一账户取走300元.倘 ...
- SQL SERVER 2008 数据库隔离级别代码演示
SQL SERVER 2008 数据库隔离级别代码演示 个隔离级别(其实这是SQL 工业标) 种隔离级别,本身没有优劣之分,完全取决于应用的场景. 本质上,他们是在 隔离性(紊乱程度) 和 灵活性 ...
- Mysql数据库事务的隔离级别和锁的实现原理分析
Mysql数据库事务的隔离级别和锁的实现原理分析 找到大神了:http://blog.csdn.net/tangkund3218/article/details/51753243 InnoDB使用MV ...
- 高性能MySQL--innodb中事务的隔离级别与锁的关系
最近买了<高性能MySQL>这本书回来看,从中收益颇多!我来一吐为快! 我们都知道事务,那么在什么情况下我们需要使用事务呢? 银行应用是解释事务的一个经典例子.假设一个银行的数据库有两张表 ...
- mysql的事务,隔离级别和锁
事务就是一组一起成功或一起失败的sql语句.事务还应该具备,原子性,一致性,隔离性和持久性. 一.事务的基本要素 (ACID) 1.原子性:事务开始后,所有的操作,要么全部成功,要么全部失败,不可能处 ...
- MySQL 事务的隔离级别及锁操作的一点点演示
MySQL 版本:5.7 安装环境:MAC OS 一.测试数据 测试数据库:test:测试表:tt CREATE TABLE `tt` ( `id` int(11) DEFAULT NULL, `na ...
- SQL Server 事务隔离级别详解
标签: SQL SEERVER/MSSQL SERVER/SQL/事务隔离级别选项/设置数据库事务级别 SQL 事务隔离级别 概述 隔离级别用于决定如果控制并发用户如何读写数据的操作,同时对性能也有一 ...
随机推荐
- [故障公告]14:39-15:39博客站点部分负载均衡遭遇3次20G以上的流量攻击
非常抱歉,今天下午14:39-15:39左右,博客站点的部分负载均衡遭遇3次20G以上的流量攻击,造成很多用户不能正常访问.由此给您带来麻烦,请您谅解. 攻击的过程是这样的: 14:39,第1次攻 ...
- Python标准库--Scope
作者:zhbzz2007 出处:http://www.cnblogs.com/zhbzz2007 欢迎转载,也请保留这段声明.谢谢! 1 模块简介 你一定在很多计算机科学课程上听说过作用域.它很重要, ...
- 初次尝试Linux并记录一二
假如我有一个Linux系统 安装过程:加载中... 版本:Ubuntu Server 16.04.1 LTS 64位 得到一个IP:*.*.*.* 下载工具 WinSCP: WinSCP是一个Wind ...
- Natas Wargame Level 2 Writeup 与目录泄露(强制访问)
- .Net程序员学用Oracle系列(29):PLSQL 之批量应用和系统包
1.批量数据操作 1.1.批量生成数据 1.2.批量插入数据 2.批量生成脚本 3.生成数据字典 4.常见系统包 4.1.DBMS_OUTPUT 4.2.DBMS_RANDOM 4.3.其它系统包及常 ...
- java中几种获取项目路径方式
转自http://caodaoxi.iteye.com/blog/1234805 在jsp和class文件中调用的相对路径不同. 在jsp里,根目录是WebRoot 在class文件中,根目录 ...
- 开涛spring3(6.7) - AOP 之 6.7 通知顺序
如果我们有多个通知想要在同一连接点执行,那执行顺序如何确定呢?Spring AOP使用AspectJ的优先级规则来确定通知执行顺序.总共有两种情况:同一切面中通知执行顺序.不同切面中的通知执行顺序. ...
- JMETER 不同线程组 变量值 的参数传递
线程组 1 在线程组1中使用__setProperty函数设置jmeter属性值(此值为全局变量值),将所需变量值如${token}设置为jmeter属性值,即newtoken,示例: 1.添加- ...
- 【Android】又一个Gank客户端来啦
介绍 Gank平台的移动端又来了,非常感谢Gank平台开放接口,让我们这些小白有机会练手.学习. 本项目在架构方面有稍微花点心思,虽然还是最简单的MVC模式,但基本参考MVP的思想,Activity只 ...
- Node.js 8有哪些重要功能和修复?
欢迎大家持续关注葡萄城控件技术团队博客,更多更好的原创文章尽在这里~~ 5月30日12点,Node.js 8正式发布了,这个版本具有一系列新功能和性能改进,并且这些功能和改进将获得长期支持(LTS). ...