SQL Server 事务隔离级别的解析

近来在项目中遇到的一些有关事务的问题,跟同事间讨论了一下,后面翻看了一些书籍和做了一些测试,趁有点时间把它写下来,一来加深印象,二来希望对大家有所帮助,当然,由于自身水平问题,如理解有误,还请大牛指出, 本人在此先行谢过.

事情首先是这样引起的, 同事写的一个导入,但在导入的过程中,由于要插多条数据,当有些数据未能插入时,却没有回滚所有的数据,而是往下执行,这样问题就来了,无法得知系统到底导了多少条记录.用户需要的结果是,要么都导入成功,要么都不成功.

if OBJECT_ID(tb1) is not null  
drop table tb1  
go  
create table tb1(id int identity primary key ,name varchar(10))  
   
 -- set xact_abort on   
 begin transaction test
 insert into tb1 values(getonjew);  
 insert into tb1 values(caozx);  
 insert into tb1 values(kevin-xxxxxxxx);  
 insert into tb1 values(toby);  
 insert into tb1 values(idawong);  
 commit transaction test
 
 
 go 
 select * from tb

以上代码得出信息与结果

信息

(1 row(s) affected)

(1 row(s) affected)

Msg 8152, Level 16, State 14, Line 7 String or binary data would be truncated. The statement has been terminated.

(1 row(s) affected)

(1 row(s) affected)

(4 row(s) affected)

结果(这里大家要注意下,即使第三条数据未成功,但其ID已被占用)

1 getonjew
2 caozx
4 toby
5 idawong

这里就是与项目中遇到的情况类似了,但这不是我们想要的结果,如果想达到想要的结果,只需要把上面的注释行.set xact_abort on  打开即可了. 但这不是这里所要讲述的. 有关事务的问题,我重新的梳理了一下. 现整理如下.请大家批评指证.

SQL事务级别用于控制并发用户如何读写数据的操作,同时对性能也有一定的影响作用。大家应根据实际的使用情况使用不同的级别.

事务隔离级别主要通过影响读操作来间接地影响写操作;可以在会话级别上设置事务隔离的级别,也可以在表上设置事务隔离级别。
事务隔离级别总共有6个级别:
READ UNCOMMITTED(未提交读,读脏),相当于(NOLOCK)
READ COMMITTED(已提交读,默认级别)
REPEATABLE READ(可以重复读),相当于(HOLDLOCK)
SERIALIZABLE(可序列化)
SNAPSHOT(快照)
READ COMMITTED SNAPSHOT(已经提交读隔离)
对于前四个隔离级别:READ UNCOMMITTED<READ COMMITTED<REPEATABLE READ<SERIALIZABLE
隔离级别越高,读操作的请求锁定就越严格,锁的持有时间久越长;所以隔离级别越高,一致性就越高,并发性就越低,同时性能也相对影响越大.

首先,可以在命令窗口输入  DBCC USEROPTIONS 查看目前隔离级别,这里同时能查看到一些其它的属性.

 

下面我们来说一下如何设置会话隔离级别

SET TRANSACTION ISOLATION LEVEL <ISOLATION NAME>
--设置查询表隔离
SELECT ....FROM <TABLE> WITH (<ISOLATION NAME>) 

 1.READ UNCOMMITTED

READ UNCOMMITTED:未提交读,可能读到脏数据

READ UNCOMMITTED:读操作不申请锁,运行读取未提交的修改,也就是允许读脏数据,读操作不会影响写操作请求排他锁.

下面举例说明:

建立一个Courses表, 里面包含以下数据

新建一会话(即新开一查询窗口)运行以下命令

BEGIN TRANSACTION
UPDATE Courses
SET SCORE=SCORE+7
WHERE ID=1

SELECT ID,SCORE FROM Courses
WHERE ID=1

得到以下结果

新建另一个会话,执行

/*先不添加隔离级别,默认是READ COMMITTED,由于数据之前的更新操作使用了排他锁(事务没有提交), 查询一直在等待锁释放*/
SELECT ID,SCORE FROM Courses
WHERE ID=1 

如果将隔离级别设置为

---将查询的隔离级别设置为READ UNCOMMITTED允许未提交读,读操作之前不请求共享锁。
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
SELECT ID,SCORE FROM Courses
WHERE ID=1 
--当然也可以使用表隔离,效果是一样的
SELECT ID,SCORE FROM Courses  WITH (NOLOCK)
WHERE ID=1 

得出结果

假设在会话1中对操作执行回滚操作ROLLBACK TRANSACTION,这样分数还是之前的80但是会话2中则读取到的是回滚前的分数87,这样就属于一个读脏操作.

2.READ COMMITTED

READ COMMITTED(已提交读)是SQL SERVER默认的隔离级别,可以避免读取未提交的数据,隔离级别比READ UNCOMMITTED未提交读的级别高; 该隔离级别读操作之前首先申请并获得共享锁,允许其他读操作读取该锁定的数据,但是写操作必须等待锁释放,一般读操作读取完就会立刻释放共享锁。

首先,再提交之前会话1的代码

BEGIN TRANSACTION
UPDATE Courses
SET SCORE=SCORE+7
WHERE ID=1

SELECT ID,SCORE FROM Courses
WHERE ID=1

这个时候,在会话2中,读取数据时一直在等待. 直到会话1提交了事务之后

COMMIT TRANSACTION 

此时在会话2才能读到数据,但这个时候读到的数据结果是修改后的结果,所以读的不是脏数据.

但是由于READ COMMITTED读操作一完成就立即释放共享锁,读操作不会在一个事务过程中保持共享锁,也就是说在一个事务的的两个查询过程之间有另一个会话对数据资源进行了更改,会导致一个事务的两次查询得到的结果不一致,这种现象称之为不可重复读,这个时候我们就要引入更高一级的隔离级别了.

 3.REPEATABLE READ

REPEATABLE READ(可重复读):保证在一个事务中的两个读操作之间,其他的事务不能修改当前事务读取的数据,该级别事务获取数据前必须先获得共享锁同时获得的共享锁不立即释放一直保持共享锁至事务完成,所以此隔离级别查询完并提交事务很重要。

首先,我们重置我们ID=1的数据为80,

在会话1中执行查询ID=1,将回话级别设置为REPEATABLE READ

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
BEGIN TRANSACTION
SELECT ID,SCORE FROM Courses
WHERE ID=1 

新建会话2修改ID=1的分数.

---由于会话1的隔离级别REPEATABLE READ申请的共享锁一直要保持到事务结束,所以回话2无法获取排他锁,处于等待状态
UPDATE Courses
SET SCORE=SCORE+7
WHERE ID=1

在会话1中执行下面语句,然后提交事务

SELECT ID,SCORE FROM Courses
WHERE ID=1 

COMMIT TRANSACTION

得出以下结果,结果一致.
 

会话1的两次查询得到的结果一致,前面的两个隔离级别无法得到一致的数据,此时事务已提交同时释放共享锁,会话2申请排他锁成功,对行执行更新操作.

REPEATABLE READ隔离级别保证一个事务中的两次查询到的结果一致,同时保证了丢失更新,所谓的丢失更新是:两个事务同时读取了同一个值然后基于最初的值进行计算,接着再更新,就会导致两个事务的更新相互覆盖。 例如公司开会申请会议室,两个人同时预定同一会议室,首先两个人同时查询到还有一间房间可以预定,然后两个人同时提交预定操作,事务1执行num=1-1,同时事务2也执行num=1-1最后修改num=0,这就导致两个人其中一个人的操作被另一个人所覆盖,REPEATABLE READ隔离级别就能避免这种丢失更新的现象,当事务1查询房间时事务就一直保持共享锁直到事务提交,而不是像前面的几个隔离级别查询完就不共享锁,就能避免其他事务获取排他锁。(当然这里只是举做例子,在实际开发中未必需要设置这个级别,可以在提交的过程中再到数据库验证一下,如果符合条件就订会议室,否则给出提示,根据情况而定)

4.SERIALIZABLE

SERIALIZABLE(可序列化),对于前面的REPEATABLE READ能保证事务可重复读,但是事务只锁定查询第一次运行时获取的数据资源(数据行),而不能锁定查询结果之外的行,就是原本不存在于数据表中的数据。因此在一个事务中当第一个查询和第二个查询过程之间,有其他事务执行插入操作且插入数据满足第一次查询读取过滤的条件时,那么在第二次查询的结果中就会存在这些新插入的数据,使两次查询结果不一致,这种读操作称之为幻读。 为了避免幻读需要将隔离级别设置为SERIALIZABLE

-- 先测试一下,之前的可重复读不能保证幻读
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
BEGIN TRANSACTION 
SELECT ID,SCORE FROM Courses

得到以下结果

然后在会话2插入一条数据.

INSERT INTO Courses VALUES(6,99)

返回会话1,再查询并提交事务.

SELECT ID,SCORE FROM Courses
COMMIT TRANSACTION

得出结果

第二次查询到的数据包含了会话2新插入的数据,两次查询结果不一致(验证之前的隔离级别不能保证幻读)

如果在前面设置隔离级别为SERIALIZABLE的话,则两次的查询结果会一致, 也即会话2插入的数据,不会在第二次查询结果中得到.

实验完后,重置会话级别显默认级别

SET TRANSACTION ISOLATION LEVEL READ COMMITTED

5.SNAPSHOT

SNAPSHOT快照:SNAPSHOT和READ COMMITTED SNAPSHOT两种隔离(可以把事务已经提交的行的上一版本保存在TEMPDB数据库中) SNAPSHOT隔离级别在逻辑上与SERIALIZABLE类似, READ COMMITTED SNAPSHOT隔离级别在逻辑上与 READ COMMITTED类似 不过在快照隔离级别下读操作不需要申请获得共享锁,所以即便是数据已经存在排他锁也不影响读操作。而且仍然可以得到和SERIALIZABLE与READ COMMITTED隔离级别类似的一致性;如果目前版本与预期的版本不一致,读操作可以从TEMPDB中获取预期的版本。

如果启用任何一种基于快照的隔离级别,DELETE和UPDATE语句在做出修改前都会把行的当前版本复制到TEMPDB中,而INSERT语句不需要在TEMPDB中进行版本控制,因为此时还没有行的旧数据

无论启用哪种基于快照的隔离级别都会对更新和删除操作产生性能的负面影响,但是有利于提高读操作的性能因为读操作不需要获取共享锁;

 

5.1 SNAPSHOT

SNAPSHOT 在SNAPSHOT隔离级别下,当读取数据时可以保证操作读取的行是事务开始时可用的最后提交版本 同时SNAPSHOT隔离级别也满足前面的已提交读,可重复读,不幻读;该隔离级别实用的不是共享锁,而是行版本控制 使用SNAPSHOT隔离级别首先需要在数据库级别上设置相关选

在打开的所有查询窗口中执行以下操作

ALTER DATABASE TEST SET ALLOW_SNAPSHOT_ISOLATION ON;

然后

在会话1中打开事务,将ID=1的分数加7,并查询跟新后的分数
BEGIN TRANSACTION
UPDATE Courses
SET SCORE=SCORE+7
WHERE ID=1

SELECT  ID,SCORE FROM Courses
WHERE ID=1
---查询到更新后的分数87

---在会话2中将隔离级别设置为SNAPSHOT,并打开事务(此时查询也不会因为会话1的排他锁而等待,依然可以查询到数据)
SET TRANSACTION ISOLATION LEVEL SNAPSHOT
BEGIN TRANSACTION
SELECT  ID,SCORE FROM Courses
WHERE ID=1


---查询到的结果还是会话1修改前的分数,由于会话1在默认的READ COMMITTED隔离级别下运行,SQL SERVER必须在更新前把行的一个副本复制到TEMPDB数据库中
--在SNAPSHOT级别启动事务会请求行版本

---现在在会话1中执行提交事务,此时ID=1的分数是87
COMMIT TRANSACTION

---再次在会话2中查询ID=1的分数并提交事务,结果还是80,因为事务要保证两次查询的结果相同

SELECT  ID,SCORE FROM Courses
WHERE ID=1

COMMIT TRANSACTION

---此时如果在回话2中重新打开一个事务,查询到的ID=1的分数是87
BEGIN TRANSACTION
SELECT  ID,SCORE FROM Courses
WHERE ID=1

COMMIT TRANSACTION

/*SNAPSHOT隔离级别保证操作读取的行是事务开始时可用的最后已提交版本,由于会话1的事务未提交,所以ID=1的最后提交版本还是修改前的分数80,
所以会话2读取到的价格是会话2事务开始前的已提交版本分数80,当会话1提交事务后,会话2重新新建一个事务此时事务开启前的分数已经是87了,
所以查询到的分数是87,同时SNAPSHOT隔离级别还能保证SERIALIZABLE的隔离级别
*/

5.2 READ COMMITTED SNAPSHOT

READ COMMITTED SNAPSHOT也是基于行版本控制,但是READ COMMITTED SNAPSHOT的隔离级别是读操作之前的最后已提交版本,而不是事务前的已提交版本,有点类似前面的READ COMMITTED能保证已提交读,但是不能保证可重复读,不能避免幻读,但是又比 READ COMMITTED隔离级别多出了不需要获取共享锁就可以读取数据
要启用READ COMMITTED SNAPSHOT隔离级别同样需要修改数据库选项,在会话1,会话2中执行以下操作(执行下面的操作当前连接必须是数据库的唯一连接,可以通过查询已连接当前数据库的进程,然后KILL掉那些进程,然后再执行该操作,否则可能无法执行成功)

 

开始前重置 ID=1的分数为80

-----在会话1中打开事务,将ID=1的分数加7,并查询跟新后的分数,并保持事务一直处于打开状态
BEGIN TRANSACTION
UPDATE Courses
SET SCORE=SCORE+7
WHERE ID=1

--查询到的分数是87,
select ID,SCORE FROM Courses
WHERE ID=1

---在会话2中打开事务查询ID=1并一直保持事务处于打开状态(此时由于会话1还未提交事务,所以会话2中查询到的还是会话1执行事务之前保存的行版本)
BEGIN TRANSACTION
select ID,SCORE FROM Courses
WHERE ID=1
--查询到的分数还是80

---在会话1中提交事务
COMMIT TRANSACTION 

---在会话2中再次执行查询ID=1的分数,并提交事务
select ID,SCORE FROM Courses
WHERE ID=1
COMMIT TRANSACTION 
--此时的分数为会话1修改后的分数87,而不是事务之前已提交版本的价格,也就是READ COMMITTED SNAPSHOT隔离级别在同一事务中两次查询的结果不一致.

 

 

 

 

 

 

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。