在今天的文章里我想谈下SQL Server里现存的各种事务隔离级别的神话和误解。主要我会谈谈下列话题:

  • 什么是事务隔离级别(Transaction Isolation Levels)?
  • NOLOCK从不阻塞!?
  • 提交读(Read Committed)不会持锁!?
  • Key Range Locks只针对可串行化?!

好,让我们从第1个开始奠定SQL Server里事务隔离级别的基础。

什么是事务隔离级别(Transaction Isolation Levels)?

每次当我站在客户角度,处理各类乱七八糟的SQL Server问题(或进行SQL Server体检),有时问题的根源是躲在数据库里的锁/阻塞行为。当你有一个糟糕的查询(可能是丢失一个非常重要的索引),整个数据库的性能都会下降。

你是否尝试过启动一个新的事务(却不曾提交它),它会获取你生产数据库主数据表的排它锁(exclusive lock)?相信我:你数据库的性能和生产力就会挂掉——就在你面前!!!因此听我老人言:不要这样做!

当我们在任何关系数据库里谈论锁和阻塞,我们也要谈到那个数据库管理系统(DBMS)所支持的各种事务隔离级别(这里我们指的是SQL Server)。SQL Server支持2中并发模式:“老”的悲观并发控制,还有自SQL Server 2005起引入“新”的乐观并发控制——一晃就是10年前的事了……

今天我想专心讲下“老”的悲观并发控制。在老的悲观并发控制模式里,SQL Server支持4个不同的事务隔离级别:

  • 未提交读/脏读(Read Uncommitted)
  • 提交读(Read committed)
  • 可重复读(Repeatable Read)
  • 可串行化(Serializable)

我不想深入讲解每个事务隔离级别,但我会给你每个隔离级别内部操作的概况,因为接下来的文章会用到那些信息。

当我们站在SQL Server的高度来看,你的事务包括读取数据(SELECT查询),还有修改数据(INSERT,UPDATE,DELETE,MERGE)。每次当你读取数据时,SQL Server需要获得共享锁(Shared Locks (S))。每次当你修改数据时,SQL Server需要获得排它锁(Exclusive Locks (X))。这2个锁是相互排斥的,就是说读操作会阻塞写操作,写操作会阻塞读操作。

现在使用事务隔离级别你就可以进行控制,读操作可以持共享(S)锁多少时间。写操作总要获得排它(X)锁,你不能影响它。SQL Server默认使用的是提交读(Read Committed)隔离级别,这意味着当你读取一条记录时,SQL Server需要获得共享(S)锁,当记录读取完成,共享(S)锁就会释放。当你逐行读取时,共享(S)锁也是逐行获取与释放。

当你不想读取操作获得共享(S)锁(事实并不推荐这样做),你可以使用未提交读/脏读(Read Uncommitted)隔离级别。未提交读/脏读意味着你可以读取脏数据——尚未提交的数据。这是贼快的(每人可以阻塞你),但是另一方面是非常危险的,因为这是未提交的数据。想下,如果未提交的事务在你读到数据后又撤销了:在你手里的数据在数据库里逻辑上并不存在。现在你的手非常的脏。在我处理SQL Server问题里,我看到很多用户使用未提交读/脏读或NOLOCK查询提示来避免SQL Server里的阻塞。那不是首选处理阻塞的方法。接下来你会看到,即使NOLOCK也会阻塞……

在提交读里你会有所谓的不可重复读(Non Repeatable Reads),因为当你在你的事务里2次读取数据时,其他人可以修改数据。如果你想避免不可重复读,你可以使用可重复读(Repeatable Read)隔离级别,在可重复读里,SQL Server持有共享(S)锁,直到你用COMMITROLLBACK来结束你的事务。这意味这没有人可以修改你读取的数据,对于你的事务,你是可以重复读的。

到目前为止在每个我们讨论的隔离级别里,你都会得到所谓的虚影记录(Phantom Records)——在你的记录集里可以出现又消失的记录。如果你想避免这些虚影记录,你必须使用可串行化(Serializable)隔离级别,最有限制的隔离级别。在可串行化里SQL Server使用所谓的Key Range Locking来消除虚影记录:你锁定整个范围的数据,因此没有其他并发的事务可以插入其它记录来阻止虚影记录。

从这个介绍可以看到,你的隔离级别越多限制,你数据库的并发操作会更受影响。因此你要正确选择对的隔离级别。一直使用提交读没有意义,一直使用可串行化也没有意义。和往常一样,依具体情况而定。

现在我已经奠定了SQL Server里事务隔离级别的基础,现在我会给你展示3个不同的情况,和我上述介绍的情况不会符合。在一些特定情况下,SQL Server会通过对特定SQL语句在底层(Under the Hood)改变事务隔离级别来保证你事务的准确性。我们开始吧……

NOLOCK从不阻塞!?

在简介里我已经描述了,对于特定的SQL语句,在数据读取时,NOLOCK查询提示会阻止需要的共享(S)锁。这会让你的SQL语句非常快,因为SQL查询不会被任何其他事务阻塞。我把NOLOCK称为SQL Server里的加速器。

但遗憾的是,当你使用并发的像ALTER TABLE的DDL语句(数据定义语言(Data Definition Language,DDL))。在我们理解这个行为前,我们需要详细看下DDL语句,当我们执行简单SELECT查询时,在SQL Server内部发生了什么。

当你使用ALTER TABLE的DDL语句修改表时,SQL Server在那个表上获得所谓架构修改锁(Schema Modification Lock (Sch-M)) 。当你现在对同个表同时运行SELECT查询时,SQL Server第1步需要编译物理执行计划。在SQL Server编译阶段需要所谓的架构修改锁(Schema Modification Lock (Sch-M))

这2个锁(Sch-M和Sch-S)是彼此互斥的!这意味着即使NOLOCK语句也会阻塞,因为在第1步你需要编译执行计划。因此在SQL Server知道物理执行计划前,你的NOLOCK语句会阻塞。当你在生产系统里升级你的数据库架构时,你有考虑过这个行为么?好好考虑下……让我们用一个简单的例子演示下这个行为。在第1步我会创建一个新表并往里插入一些记录:

 1 -- Create a new test table
 2 CREATE TABLE TestTable
 3 (
 4     Column1 INT,
 5     Column2 INT,
 6     Column3 INT
 7 )
 8 GO
 9
10 -- Insert some test data
11 DECLARE @i INT = 0
12
13 WHILE (@i < 10000)
14 BEGIN
15     INSERT INTO TestTable VALUES (@i, @i + 1, @i + 2)
16     SET @i += 1
17 END
18 GO

然后我们通过执行一个ALTER TABLE的DDL语句来开始一个新的事务,给我们的表增加一个新列:

1 -- Begin a new transaction and do some work
2 BEGIN TRANSACTION
3
4 -- Add a new column
5 -- DDL statements require a Sch-M lock on the objects that are modified.
6 -- In this case, the table "TestTable" gets a Sch-M lock (Schema modification lock)!
7 ALTER TABLE TestTable ADD Column4 INT

从刚才的代码可以看到,这个事务还在进行中,尚未提交。因此让我们在SSMS里打开新的会话来执行下列代码:

1 -- The statement is now blocking, even with the NOLOCK query hint!
2 -- SQL Server has to compile the query, and requests a Sch-S lock (Schema Stability lock).
3 -- This lock is incompatible with the Sch-M lock!
4 SELECT * FROM TestTable WITH (NOLOCK)
5 GO

你会马上看到,这个SQL语句并没有返回一条记录,因为它被其它活动的事务阻塞——即使使用NOLOCK查询提示!对这2个会话,你可以通过查询DMV sys.dm_tran_locks对这个阻塞情况进一步故障排除。你会看到Sch-M锁阻塞了Sch-S锁。
使用NOLOCK查询提示或未提交读/脏读事务隔离级别不能保证你的SQL语句马上执行。在过去里很多用户都在与此特定问题斗争。

提交读(Read Committed)不会持锁!?

在简介里你已经学到提交读事务隔离级别,共享(S)锁只在记录读取期间把持。这意味着只要记录被读取,那个锁就会被立即释放。因此当你从一个表读取数据,针对当前被处理的记录,在这个时间点只有共享(S)锁。这个描述只在你的执行计划没有阻塞运算符——例如排序(sort)运算符是对的。当你的执行计划有这样的运算符,意味着SQL Server需要创建你数据的副本。

在数据副本完成后,原始的表/索引数据就不需要保留。但当你处理小量数据时,创建数据副本不会影响你的性能。假设当你有VARCHAR(MAX)列定义的表时。在那个情况下每行的那列可以保存最大2GB的数据。创建数据副本只会吹干你的内存和TempDb。

为了避免这个问题,SQL Server只持共享(S)锁到你语句的结束。因此在此同时不存在有人改变数据的可能(共享(S)锁阻塞排它(X)锁),SQL Server只引用原始,稳定,未改变的数据。因此,你的事务运行起来像在可重读隔离级别,这会伤及你数据库的扩展性。让我们创建下列数据库架构来演示这个行为:

 1 -- Create a new table
 2 CREATE TABLE TestTable
 3 (
 4     ID INT IDENTITY(1, 1) NOT NULL PRIMARY KEY,
 5     Col2 INT,
 6     Col3 VARCHAR(MAX)
 7 )
 8 GO
 9
10 -- Insert some records into the table
11 INSERT INTO TestTable VALUES (1, 'abc'), (2, 'def'), (3, 'ghi')
12 GO
13
14 -- Begin a new transaction, so that we are blocking some records in the table
15 BEGIN TRANSACTION
16
17 UPDATE TestTable SET Col2 = 1 WHERE ID = 3

从代码里你可以看到,我创建一个聚集表,有3条聚集键值为1,2,3的记录。下一步我会开始一个新的事务,我们在聚集索引里锁定第3条记录。现在我们搭建好演示。在独立的会话里,我们尝试从表读取记录——显然这个SELECT语句会阻塞:

1 -- This statement only acquires a key lock on the current record
2 SELECT Col3 FROM TestTable
3 GO

当我们从DMV sys.dm_tran_locks里查看,你会清楚看到那个SELECT语句在等待在聚集键值为3的第3个锁。这是提交读隔离级别的典型行为,因为在我们的执行计划里没有阻塞运算符。但只要我们引入一个阻塞运算符(并读取一个LOB数据类型),事情就会发生改变:

1 -- This statement only acquires a key lock on the current record
2 SELECT Col3 FROM TestTable
3 ORDER BY Col2
4 GO

如你所见,现在我们使用了ORDER BY字句,在执行计划里会给我们排序(sort)运算符。当然,这个SELECT语句会再次阻塞。但当我们查看DMV sys.dm_tran_locks时,你会看到提交读隔离级别完全不同的行为:SQL Server现在在前2行(聚集键值为1,2)获得共享(S)锁,但不再释放它们了!SQLServer把持这些锁直到我们的SELECT语句完成,这是为了阻止对潜在数据的并发改变。

在可重读隔离级别里,我们的SELECT语句会高效运行。当你设计你的下个表架构时,考虑下这点,还有在你主要事务表里包含LOB数据类型时也是。

Key Range Locks只针对序列化?!

前段时间我碰到一个问题(在数据库体检期间),在提交读事务这个默认隔离级别里碰到Key Range Locks。从这个文章的开始,我们就知道在可串行化隔离级别才会用到Key Range Locks。因此问题是这些Key Range Locks从哪里来的。

当我们进一步分析数据架构时,我们发现表用了外键约束,在那里级联删除(Cascading Deletes)被启用。只要你对外键启用级联删除,当你从父表里删除记录时,SQL Server就会在子表上使用Key Range Locks。这是对的,因为在级联删除期间,Key Range Locks可以阻止新记录的插入。因此表的引用完整性被保持。我们来看一个具体的例子。在第1步我们创建2个表,在2个表之间定义外键,并启用级联删除。

 1 -- Create a new parent table
 2 CREATE TABLE Parent
 3 (
 4     Parent1 INT PRIMARY KEY NOT NULL,
 5     Parent2 INT NOT NULL
 6 )
 7 GO
 8
 9 -- Create a new child table
10 CREATE TABLE Child
11 (
12     Child1 INT PRIMARY KEY NOT NULL,
13     Child2 INT NOT NULL,
14     -- The following column will contain a Foreign Key constraint
15     Parent1 INT NOT NULL
16 )
17 GO
18
19 -- Create a foreign key constraint between both tables,
20 -- and enable Cascading Deletes on it
21 ALTER TABLE Child
22 ADD CONSTRAINT FK_Child_Parent
23 FOREIGN KEY (Parent1) REFERENCES Parent(Parent1)
24 ON DELETE CASCADE
25 GO
26
27 -- Insert some test data
28 INSERT INTO Parent VALUES (1, 1), (2, 2), (3, 3)
29 INSERT INTO Child VALUES (1, 1, 1), (2, 2, 1), (3, 3, 1)
30 GO

现在当你开始新的事务,你从父表里删除值为1的记录,SQL Server也会从子表里删除对应的记录(在这里是3条),因为我们启用了级联删除:

 1 -- Start a new transaction and analyze the acquired locks
 2 BEGIN TRANSACTION
 3
 4 -- This statement deletes the record from the parent table,
 5 -- and the 3 records from the child table
 6 DELETE FROM Parent
 7 WHERE Parent1 = 1
 8
 9 -- SQL Server uses 3 RangeX-X locks, even with the default
10 -- Isolation Level of Read Committed
11 SELECT * FROM sys.dm_tran_locks
12 WHERE request_session_id = @@SPID
13
14 COMMIT
15 GO

在事务执行期间,你可以查看下DMV sys.dm_tran_locks,你可以看到你的会话需要获取3个RangeX-X锁——Key Range Locks!在删除期间,SQL Server需要这些锁来阻止新记录的插入。我们在这里可以看到,SQL Server明显把你的事务隔离级别提升到可串行化来保证你事务的准确性。

小结

从这个文章可以看出,在SQL Server是没有任何保证的。我经常问用户,在SQL Server里,你们知道各个隔离级别,锁行为的具体信息么。如果知道的话,我们就可以一起面对这篇文章里提到的不同现象。

从这里得出的最重要的结果是,对于提供的SQL语句,SQL Server是能提升隔离级别的。因此不要以为SQL Server总是在你设置的隔离级别里运行你的查询。

感谢关注!

参考文章:

https://www.sqlpassion.at/archive/2014/01/21/myths-and-misconceptions-about-transaction-isolation-levels/

事务隔离级别神话与误解相关推荐

  1. pg事务:隔离级别历史与SSI

    事务隔离级别的历史 ANSI SQL-92定义的隔离级别和异常现象确实对数据库行业影响深远,甚至30年后的今天,绝大部分工程师对事务隔离级别的概念还停留在此,甚至很多真实的数据库隔离级别实现也停留在此 ...

  2. MySQL不同事务隔离级别对读数异常(脏读等)的拦截情况

    一直以来,对事务的隔离级别的理解一直有点模糊,今天借此文对这一块的知识点进行梳理,并配合相关实验进行说明,以期加深理解.知识点部分的文字采集于网络,文末附上参考链接,感谢大神的贡献! 在说明举例事务隔 ...

  3. Java面试题之Oracle 支持哪三种事务隔离级别

    Oracle 支持三种事务隔离级别: 1.读已提交:(默认) 2.串行化: 3.只读模式 转载于:https://www.cnblogs.com/hujinshui/p/10463883.html

  4. mysql 事务隔离规范_MySQL事务隔离级别以及脏读、幻读、不可重复读示例

    事务的隔离性 MySQL是一个客户端/服务器架构的软件,对于同一个服务器来说,可以有若干个客户端与之连接,每个客户端与服务器连接上之后,就可以称之为一个会话(Session).每个客户端都可以在自己的 ...

  5. SQL SERVER的锁机制(三)——概述(锁与事务隔离级别)

    五.锁与事务隔离级别 事务隔离级别简单的说,就是当激活事务时,控制事务内因SQL语句产生的锁定需要保留多入,影响范围多大,以防止多人访问时,在事务内发生数据查询的错误.设置事务隔离级别将影响整条连接. ...

  6. 关于MySQL的四种事务隔离级别!

    本文实验的测试环境:Windows 10+cmd+MySQL5.6.36+InnoDB 一.事务的基本要素(ACID) 原子性(Atomicity):事务开始后所有操作,要么全部做完,要么全部不做,不 ...

  7. pb设置Oracle事务的隔离级别,Oracle的事务隔离级别

    ANSI/ISO SQL规定了四种事务隔离级别,分别是:read uncommitted,read committed,repeatable read,serializable ORACE提供了SQ9 ...

  8. SQL Server 中的事务与事务隔离级别以及如何理解脏读, 未提交读,不可重复读和幻读产生的过程和原因...

    原本打算写有关 SSIS Package 中的事务控制过程的,但是发现很多基本的概念还是需要有 SQL Server 事务和事务的隔离级别做基础铺垫.所以花了点时间,把 SQL Server 数据库中 ...

  9. mysql选什么隔离级别_互联网项目中mysql应该选什么事务隔离级别

    摘要 企业千万家,靠谱没几家. 社招选错家,亲人两行泪. 祝大家金三银四跳槽顺利! 引言 开始我们的内容,相信大家一定遇到过下面的一个面试场景 面试官:"讲讲mysql有几个事务隔离级别?& ...

最新文章

  1. 【HZOI2015】帕秋莉的超级多项式
  2. 超大数据下大批量随机键值的查询优化方案
  3. ORACLE SQL开发where子句之case-when
  4. Java并发编程的艺术(一)——并发编程需要注意的问题
  5. Android应用开发基础篇(1)-----Button
  6. 201107阶段二Linux-qt编程
  7. 深度学习之基于opencv和CNN实现人脸识别
  8. 7-4 二叉树的遍历!(简单) (25 分)
  9. 强大的漏洞扫描工具--nessus
  10. DZNEmptyDataSet,优秀的空白页或者出错页封装
  11. cocos2d-x中使用可加密Sqlite存储玩家数据
  12. centos7 python3 爬虫登陆邮箱_Centos7搭建Scrapy爬虫环境
  13. 赛题解读 | 如何基于 Flink + AI 解决疫情防控难题?
  14. 网页前端:JavaScript编程
  15. 使用Bugly 作为APP异常上报工具
  16. 信用社考试计算机知识点,2017年农村信用社计算机考试模拟题
  17. 用php语句绘制圆锥,JS、canvas画一个圆锥实现代码
  18. C/C++基础进阶篇:C++11 中引入的 delete 描述符使用场景
  19. YOLOV5测试及训练自己的数据集
  20. Linux命令行如何设置MySQL远程连接

热门文章

  1. MySQL DATE_ADD() 函数
  2. iOS-Senior20-Map定位
  3. node.js 安装 测试
  4. 091116 T 维护性
  5. 美国《预防》杂志近期帮大家总结了11种自然疗法来对抗高血压
  6. IMPORTANT: BizTalk Server 2004 fixes for Windows XP SP2 and .NET FW 1.1 SP1
  7. 队列定义、队列的存储结构
  8. Windows下安装配置ant
  9. FastJson PropertyNamingStrategy 转换成JSON字符串的四种格式配置及源码解析
  10. 浅谈浏览器多进程与JS线程 1