SqlServer教程

SQL Server事务隔离级别详解

本文主要是介绍SQL Server事务隔离级别详解,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

SQL Server事务隔离级别详解


SQL 事务隔离级别

概述

     隔离级别用于决定如果控制并发用户如何读写数据的操作,同时对性能也有一定的影响作用。

步骤

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

获取事务隔离级别(isolation level)

DBCC USEROPTIONS

 

设置隔离

设置回话隔离 SET TRANSACTION ISOLATION LEVEL <ISOLATION NAME>

--注意:在设置回话隔离时(REPEATABLE READ)两个单词需要用空格间隔开,但是在表隔离中可以粘在一起(REPEATABLEREAD)

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

 

1.READ UNCOMMITTED

READ UNCOMMITTED:未提交读,读脏数据
默认的读操作:需要请求共享锁,允许其他事物读锁定的数据但不允许修改.
READ UNCOMMITTED:读操作不申请锁,运行读取未提交的修改,也就是允许读脏数据,读操作不会影响写操作请求排他锁.

 创建测试数据

复制代码

IF OBJECT_ID('Orders','U') IS NOT NULL DROP TABLE Orders

GO

CREATE TABLE Orders
(ID INT NOT NULL,
Price FLOAT NOT NULL );

INSERT INTO Orders VALUES(10,10.00),(11,11.00),(12,12.00),(13,13.00),(14,14.00);

GO SELECT ID,Price FROM Orders

 

新建回话1将订单10的价格加1

复制代码

BEGIN TRANSACTION UPDATE Orders SET Price=Price+1 WHERE ID=10 SELECT ID,Price FROM Orders WHERE ID=10

复制代码

在另一个回话2中执行查询操作

复制代码

首先不添加隔离级别,默认是READ COMMITTED,由于数据之前的更新操作使用了排他锁,所以查询一直在等待锁释放*/ SELECT ID,Price FROM Orders WHERE ID=10 ---将查询的隔离级别设置为READ UNCOMMITTED允许未提交读,读操作之前不请求共享锁。 SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED SELECT ID,Price FROM Orders WHERE ID=10; --当然也可以使用表隔离,效果是一样的 SELECT ID,Price FROM Orders WITH (NOLOCK) WHERE ID=10

复制代码

假设在回话1中对操作执行回滚操作,这样价格还是之前的10,但是回话2中则读取到的是回滚前的价格11,这样就属于一个读脏操作

ROLLBACK TRANSACTION

 

2.READ COMMITTED

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

新建回话1将订单10的价格加1,此时回话1的排他锁锁住了订单10的值

复制代码

BEGIN TRANSACTION UPDATE Orders SET Price=Price+1 WHERE ID=10 SELECT ID,Price FROM Orders WHERE ID=10

复制代码

在回话2中执行查询,将隔离级别设置为READ COMMITTED

复制代码

SET TRANSACTION ISOLATION LEVEL READ COMMITTED SELECT ID,Price FROM Orders WHERE ID=10 ---由于READ COMMITTED需要申请获得共享锁,而锁与回话1的排他锁冲突,回话被堵塞, ----在回话1中执行事务提交 COMMIT TRANSACTION /*由于回话1事务提交,释放了订单10的排他锁,此时回话2申请共享锁成功查到到订单10的价格为修改后的价格11,READ COMMITTED由于是已提交读隔离级别,所以不会读脏数据. */

复制代码

重置数据

UPDATE Orders SET Price=10 WHERE ID=10

 

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

3.REPEATABLE READ

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

在回话1中执行查询订单10,将回话级别设置为REPEATABLE READ

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ BEGIN TRANSACTION SELECT ID,Price FROM Orders WHERE ID=10

 

新建回话2修改订单10的价格

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

 

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

SELECT ID,Price FROM Orders WHERE ID=10 COMMIT TRANSACTION

 

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

REPEATABLE READ隔离级别保证一个事务中的两次查询到的结果一致,同时保证了丢失更新
丢失更新:两个事务同时读取了同一个值然后基于最初的值进行计算,接着再更新,就会导致两个事务的更新相互覆盖。
例如酒店订房例子,两个人同时预定同一酒店的房间,首先两个人同时查询到还有一间房间可以预定,然后两个人同时提交预定操作,事务1执行number=1-0,同时事务2也执行number=1-0最后修改number=0,这就导致两个人其中一个人的操作被另一个人所覆盖,REPEATABLE READ隔离级别就能避免这种丢失更新的现象,当事务1查询房间时事务就一直保持共享锁直到事务提交,而不是像前面的几个隔离级别查询完就是否共享锁,就能避免其他事务获取排他锁。

 4.SERIALIZABLE

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

复制代码

IF OBJECT_ID('Orders','U') IS NOT NULL DROP TABLE Orders GO CREATE TABLE Orders
(ID INT NOT NULL PRIMARY KEY,
Price FLOAT NOT NULL,
type INT NOT NULL ); INSERT INTO Orders VALUES(10,10.00,1),(11,11.00,1),(12,12.00,1),(13,13.00,1),(14,14.00,1); GO

复制代码

在回话1中执行查询操作,并将事务隔离级别设置为REPEATABLE READ(先测试一下前面更低级别的隔离)

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ BEGIN TRANSACTION SELECT ID,Price,type FROM Orders WHERE TYPE=1

 

在回话2中执行插入操作

INSERT INTO Orders VALUES(15,15.00,1)

 

返回回话1重新执行查询操作并提交事务

SELECT ID,Price,type FROM Orders WHERE TYPE=1 COMMIT TRANSACTION

 

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

重新插入测试数据

复制代码

IF OBJECT_ID('Orders','U') IS NOT NULL DROP TABLE Orders GO CREATE TABLE Orders
(ID INT NOT NULL PRIMARY KEY,
Price FLOAT NOT NULL,
type INT NOT NULL ); INSERT INTO Orders VALUES(10,10.00,1),(11,11.00,1),(12,12.00,1),(13,13.00,1),(14,14.00,1); GO

复制代码

接下来将回话级别设置为SERIALIZABLE,在回话1中执行查询操作,并将事务隔离级别设置为SERIALIZABLE

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE BEGIN TRANSACTION SELECT ID,Price,type FROM Orders WHERE TYPE=1

 

在回话2中执行插入操作

INSERT INTO Orders VALUES(15,15.00,1)

 

返回回话1重新执行查询操作并提交事务

SELECT ID,Price,type FROM Orders WHERE TYPE=1 COMMIT TRANSACTION

 

两次执行的查询结果相同

 

重置所有打开回话的默认隔离级别

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.1SNAPSHOT

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

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

ALTER DATABASE TEST SET ALLOW_SNAPSHOT_ISOLATION ON;

 

重置测试数据

复制代码

IF OBJECT_ID('Orders','U') IS NOT NULL DROP TABLE Orders GO CREATE TABLE Orders
(ID INT NOT NULL PRIMARY KEY,
Price FLOAT NOT NULL,
type INT NOT NULL ); INSERT INTO Orders VALUES(10,10.00,1),(11,11.00,1),(12,12.00,1),(13,13.00,1),(14,14.00,1); GO

复制代码

复制代码

在回话1中打开事务,将订单10的价格加1,并查询跟新后的价格 BEGIN TRANSACTION UPDATE Orders SET Price=Price+1 WHERE ID=10 SELECT ID,Price,type FROM Orders WHERE ID=10 ---查询到更新后的价格为11 ---在回话2中将隔离级别设置为SNAPSHOT,并打开事务(此时查询也不会因为回话1的排他锁而等待,依然可以查询到数据) SET TRANSACTION ISOLATION LEVEL SNAPSHOT BEGIN TRANSACTION SELECT ID,Price,type FROM Orders WHERE ID=10 ---查询到的结果还是回话1修改前的价格,由于回话1在默认的READ COMMITTED隔离级别下运行,SQL SERVER必须在更新前把行的一个副本复制到TEMPDB数据库中 --在SNAPSHOT级别启动事务会请求行版本 ---现在在回话1中执行提交事务,此时订单10的价格为11 COMMIT TRANSACTION ---再次在回话二中查询订单10的价格并提交事务,结果还是10,因为事务要保证两次查询的结果相同 SELECT ID,Price,type FROM Orders WHERE ID=10 COMMIT TRANSACTION ---此时如果在回话2中重新打开一个事务,查询到的订单10的价格则是11 BEGIN TRANSACTION SELECT ID,Price,type FROM Orders WHERE ID=10 COMMIT TRANSACTION /*SNAPSHOT隔离级别保证操作读取的行是事务开始时可用的最后已提交版本,由于回话1的事务未提交,所以订单10的最后提交版本还是修改前的价格10,所以回话2读取到的价格是回话2事务开始前的已提交版本价格10,当回话1提交事务后,回话2重新新建一个事务此时事务开启前的价格已经是11了,所以查询到的价格是11,同时SNAPSHOT隔离级别还能保证SERIALIZABLE的隔离级别*/

复制代码

5.2READ COMMITTED SNAPSHOT

READ COMMITTED SNAPSHOT也是基于行版本控制,但是READ COMMITTED SNAPSHOT的隔离级别是读操作之前的最后已提交版本,而不是事务前的已提交版本,有点类似前面的READ COMMITTED能保证已提交读,但是不能保证可重复读,不能避免幻读,但是又比 READ COMMITTED隔离级别多出了不需要获取共享锁就可以读取数据

要启用READ COMMITTED SNAPSHOT隔离级别同样需要修改数据库选项,在回话1,回话2中执行以下操作(执行下面的操作当前连接必须是数据库的唯一连接,可以通过查询已连接当前数据库的进程,然后KILL掉那些进程,然后再执行该操作,否则可能无法执行成功)

复制代码

ALTER DATABASE TEST SET READ_COMMITTED_SNAPSHOT ON IF OBJECT_ID('Orders','U') IS NOT NULL DROP TABLE Orders GO CREATE TABLE Orders
(ID INT NOT NULL PRIMARY KEY,
Price FLOAT NOT NULL,
type INT NOT NULL ); INSERT INTO Orders VALUES(10,10.00,1),(11,11.00,1),(12,12.00,1),(13,13.00,1),(14,14.00,1); GO -----在回话1中打开事务,将订单10的价格加1,并查询跟新后的价格,并保持事务一直处于打开状态 BEGIN TRANSACTION UPDATE Orders SET Price=Price+1 WHERE ID=10 --查询到的价格是11 SELECT ID,Price,type FROM Orders WHERE ID=10 ---在回话2中打开事务查询订单10并一直保持事务处于打开状态(此时由于回话1还未提交事务,所以回话2中查询到的还是回话1执行事务之前保存的行版本) BEGIN TRANSACTION SELECT ID,Price,type FROM Orders WHERE ID=10 --查询到的价格还是10 ---在回话1中提交事务 COMMIT TRANSACTION ---在回话2中再次执行查询订单10的价格,并提交事务 SELECT ID,Price,type FROM Orders WHERE ID=10 COMMIT TRANSACTION --此时的价格为回话1修改后的价格11,而不是事务之前已提交版本的价格,也就是READ COMMITTED SNAPSHOT隔离级别在同一事务中两次查询的结果不一致.

复制代码

关闭所有连接,然后打开一个新的连接,禁用之前设置的数据库快照隔离级别选项

ALTER DATABASE TEST SET ALLOW_SNAPSHOT_ISOLATION OFF; ALTER DATABASE TEST SET READ_COMMITTED_SNAPSHOT OFF;

 

 

总结

   理解了事务隔离级别有助于理解事务的死锁。

 

 

备注:

    作者:pursuer.chen

    博客:http://www.cnblogs.com/chenmh

本站点所有随笔都是原创,欢迎大家转载;但转载时必须注明文章来源,且在文章开头明显处给明链接,否则保留追究责任的权利。

《欢迎交流讨论》





 

SQL Server 之 事务与隔离级别实例讲解

  SQL Server 实现了6个隔离级别来防止并发情况下,类似企图并发的访问或修改同一数据时问题的发生。本文将带你体验全部6个隔离级别。正如你接下来将看到的,你将理解每个隔离级别所能达成的效果以及何时使用它。

一、事务简介

  SQL Server的6个隔离级别中有5个是用于隔离事务的,它们因而被称作事务隔离级别。另外的一个工作于语句级别。

  在现实中要求多个数据修改操作必须要么完全成功要么什么也没发生的例子。当数据被合并到数据库时,可能有多个表需要更新。当顾客下订单时,Order表、Invoice Line Item表和Product表的数据可能都需要更新。购买机票也许要求更新Passenger表和Reservations表。无论何时当一个操作要求多个数据更改操作整体地作为单一的单元来处理,这就是需要使用事务的时候。

  如果事务中所有的数据更改操作都成功了,那么这些数据更改就可以被提交(也就是持久化到数据库)。否则,截止到失败点事务中所发生的所有数据更改必须被回滚(也就是撤销操作,什么也不曾发生)。
  事务相关操作,参照 http://www.cnblogs.com/xinaixia/p/4831198.html 。

二、 隔离级别简介

  必须小心对待并发情况,因为它们可能引发已知的并发性问题,包括“脏读”、“不可重复读”和“幻像读”,这些问题可能反过来导致数据的不良后果。正如我们已经知道的,为了防止并发性问题,隔离级别用于将事务或语句相互间隔离开来。

  下面是SQL Server 2008中定义的隔离级别名称:

  1、Transaction Isolation Level

  [1] READ UNCOMMITTED  (未提交读,读脏),相当于(NOLOCK)
  [2] READ COMMITTED (Default)  (已提交读,默认级别)
  [3] REPEATABLE READ  (可以重复读),相当于(HOLDLOCK)
  [4] SERIALIZABLE  (可序列化)
  [5] SNAPSHOT  (快照)

  2、Statement Isolation Level

  [6] READ COMMITTED SNAPSHOT  (已经提交读隔离)


  对于前四个隔离级别:READ UNCOMMITTED < READ COMMITTED < REPEATABLE READ < SERIALIZABLE
  隔离级别越高,读操作的请求锁定就越严格,锁的持有时间久越长;所以隔离级别越高,一致性就越高,并发性就越低,同时性能也相对影响越大。

  获取事务隔离级别(isolation level)

DBCC USEROPTIONS

  设置隔离

设置会话隔离 SET TRANSACTION ISOLATION LEVEL <ISOLATION NAME> --注意:在设置回话隔离时(REPEATABLE READ)两个单词需要用空格间隔开,但是在表隔离中可以粘在一起(REPEATABLEREAD)  设置查询表隔离 SELECT ....FROM <TABLE> WITH (<ISOLATION NAME>)

  正如你在下面的例子中即将看到的,隔离级别越高,提供的保护级别也越高(防止更多的并发性问题)。并且,每个隔离级别包括了前一个级别所提供的保护,因此,每个后续的更高隔离级别以避免更多并发性问题的形式提供了额外的保护。但是,世上没有免费的午餐,隔离级别越高,数据可用性就越低。选择合适的隔离级别是一种在高度安全的并发性和数据的高可用性之间寻求平衡的行为。

三、引入实例

  为了创建并发环境,所有例子使用2个SQL Server Session,每个会话运行一个不同的事务,每个事务访问相同的资源。在SQL Server Management Studio中,每个查询窗口代表了一个不同的Session,因此,你可以在SQL Server Management Studio中为不同的事务使用不同的查询窗口。
  所有例子包含了真实场景以便你将这一切建立在现实的基础上。

  1、READ UNCOMMITTED 未提交读

  READ UNCOMMITTED 事务隔离级别根本就没有提供事务间的隔离,它允许违反并发性原则的最基本形式之一 -- 脏读。当一个事务能够读取另一个事务中已经Update但尚未Commit的数据时,“脏读”就发生了。READ UNCOMMITTED 读操作不申请锁,运行读取未提交的修改,也就是允许读脏数据,读操作不会影响写操作请求排他锁。

  READ UNCOMMITTED 常应用于:单用户系统;系统中两个事务同时访问同一资源的可能性为零或几乎为零;当使用Rowversion数据类型控制并发性时 。

  

   2、READ COMMITTED 已提交读,默认

  通过仅允许一个事务读取另一个事务中已经提交的数据,READ COMMITTED 事务隔离级别防止了“脏读”问题。这是SQL Server中默认的事务隔离级别。

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

   3、REPEATABLE READ 可重复读

  正如你在前一个事务隔离级别的步骤2所看到的,Session 2中的事务能够修改已经被Session 1中的事务读取的数据。正像真实场景中所描述的,这可能导致“LOST UPDATE”。REPEATABLE READ 事务隔离级别不允许这种情况发生,因为它违背了REPEATABLE READ原则。换句话说,Session 1中的事务读取同一数据可能会产生不同的结果。

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

   4、SERIALIZABLE 可序列化

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

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

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

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

   5、SNAPSHOT 快照 

  也许你已经注意到,在上述例1到例4中,防止并发性问题的同时也降低了数据的可访问性。先是不允许Read,然后是不允许Update,不允许Insert。SNAPSHOT事务隔离级别防止了之前那些隔离级别所能防止的许多并发性问题,同时降低了与之相关的成本。它允许更高的数据可用性。
  通过在事务开始前在TempDB中使用row versions创建一份数据库的虚拟快照,SNAPSHOT事务隔离级别完成了此壮举。此后它只允许事务访问该数据库虚拟快照。这种方法被称做“基于版本控制的隔离”(versioning-based isolation)。
  使用versioning-based isolation,事务仅能看到虚拟快照中的数据。因此,其他事务仍然能够访问同一数据,只要它们不去修改已经被第一个事务修改过的数据就好。如果那样做了(企图修改数据),那么,那些事务将会被回滚并以错误消息终止。
  只有当数据库中启用SNAPSHOT事务隔离级别的开关打开后,才能使用它。打开此开关将告知数据库去设置版本化环境。理解这一点很重要,因为,一旦版本化开启,数据库会有维护版本化的开销,无论是否有事务正在使用SNAPSHOT事务隔离级别。

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

   6、READ COMMITTED SNAPSHOT 提交读快照

  到目前为止,所有的隔离级别都是将事务相互间隔离开来。一旦初始事务完成了,对其他事务变得不可用的资源才又变得可用。READ COMMITTED SNAPSHOT 隔离级别在这点上有所不同,它能够读取其已经被他事务提交的数据。它也是通过数据库开关来打开的。然后,任何使用READ COMMITTED SNAPSHOT 隔离级别的事务将通过版本化起作用。

  READ COMMITTED SNAPSHOT也是基于行版本控制,但是READ COMMITTED SNAPSHOT的隔离级别是“ 读操作之前的最后已提交版本,而不是事务前的已提交版本 ”,有点类似前面的READ COMMITTED能保证已提交读,但是不能保证可重复读,不能避免幻读,但是又比 READ COMMITTED隔离级别多出了不需要获取共享锁就可以读取数据。
  要启用READ COMMITTED SNAPSHOT隔离级别同样需要修改数据库选项。
  

四、小结

隔离级别  解决的并发性问题存在的并发性问题
READ UNCOMMITTED不适用于并发场合Dirty Reads, Non-repeatable Reads, Phantom Reads
READ COMMITTEDDirty ReadsLost Update , Non-repeatable Reads, Phantom Reads
REPEATABLE READNon-repeatable ReadsPhantom Reads, potentially Deadlocking
SERIALIZABLEPhantom ReadsLess Data Availability, potentially Deadlocking
SNAPSHOT上述所有并发性问题事务访问的是虚拟快照,其他事务Committed的数据对当前事务仍然不可见,也不允许Update被其他事务Updated的数据。
READ COMMITTED SNAPSHOT上述所有并发性问题

分类:  SQL Server



SQL Server 中的事务与事务隔离级别以及脏读

 (1700)  (0)

原 本打算写有关 SSIS Package 中的事务控制过程的,但是发现很多基本的概念还是需要有 SQL Server 事务和事务的隔离级别做基础铺垫。所以花了点时间,把 SQL Server 数据库中的事务概念,ACID 原则,事务中常见的问题,问题造成的原因和事务隔离级别等这些方面的知识好好的整理了一下。

其实有关 SQL Server 中的事务,说实话因为内容太多, 话题太广,稍微力度控制不好就超过了我目前知识能力范围,就不是三言两语能够讲清楚的。所以希望大家能够指出其中总结的不足之处,对我来说多了提高的机会,更可以帮助大家加深对事务的理解。


本文涉及到的知识点:

  • SQL Server 数据库中事务的概念
  • ACID 原则 (加了一部分内容专门解释原子性,提到了显示事务以及 XACT_ABORT 机制来确保事务的原子性)
  • 列出事务中常见的问题以及原因:脏读,未提交读,不可重复读,幻读 等
  • SQL Server中 事务的隔离级别以及它们如何做到避免脏读,未提交读,不可重复读和幻读 (用代码描述了这些问题,并且使用时间序来解释产生的原因)

SQL Server 数据库中事务的概念

数据库中的事务是数据库并发控制的基本单位,一条或者一组语句要么全部成功,对数据库中的某些数据成功修改; 要么全部不成功,数据库中的数据还原到这些语句执行

之 前的样子。比如网上订火车票,要么你定票成功,余票显示就减一张; 要么你定票失败获取取消订票,余票的数量还是那么多。不允许出现你订票成功了,余票没有减少或者你取消订票了,余票显示却少了一张的这种情况。这种不被允 许出现的情况就要求购票和余票减少这两个不同的操作必须放在一起,成为一个完整的逻辑链,这样就构成了一个事务。


数据库中事务的 ACID 原则

原子性 (Atomictiy):事务的原子性是指一个事务中包含的一条语句或者多条语句构成了一个完整的逻辑单元,这个逻辑单元具有不可再分的原子性。这个逻辑单元要么一起提交执行全部成功,要么一起提交执行全部失败。

一致性 (Consistency):可以理解为数据的完整性,事务的提交要确保在数据库上的操作没有破坏数据的完整性,比如说不要违背一些约束的数据插入或者修改行为。一旦破坏了数据的完整性,SQL Server 会回滚这个事务来确保数据库中的数据是一致的。

隔离性(Isolation):与数据库中的事务隔离级别以及锁相关,多个用户可以对同一数据并发访问而又不破坏数据的正确性和完整性。但是,并行事务的修改必须与其它并行事务的修改相互独立,隔离。 但是在不同的隔离级别下,事务的读取操作可能得到的结果是不同的。

持久性(Durability):数据持久化,事务一旦对数据的操作完成并提交后,数据修改就已经完成,即使服务重启这些数据也不会改变。相反,如果在事务的执行过程中,系统服务崩溃或者重启,那么事务所有的操作就会被回滚,即回到事务操作之前的状态。

我 理解在极端断电或者系统崩溃的情况下,一个发生在事务未提交之前,数据库应该记录了这个事务的”ID”和部分已经在数据库上更新的数据。供电恢复数据库重 新启动之后,这时完成全部撤销和回滚操作。如果在事务提交之后的断电,有可能更改的结果没有正常写入磁盘持久化,但是有可能丢失的数据会通过事务日志自动 恢复并重新生成以写入磁盘完成持久化。

原子性的进一步理解

关于原子性,有必要在这里多补充一下,因为我们描述的概念是指在事务中的原子性。一条 SQL 语句和多条 SQL 语句在处理原子性上是有一些区别的,下面演示了这些区别。

先运行这些代码,创建一个非常简单的测试表,这张表只简单模拟了一个账户的 ID 和账户余额。

复制代码      

USE BIWORK_SSIS
GO

IF OBJECT_ID('dbo.Account') IS NOT NULL
DROP TABLE dbo.Account
GO

CREATE TABLE dbo.Account
(
  ID INT PRIMARY KEY,
  AccountBalance MONEY CHECK(AccountBalance >= 0)
)

     

复制代码      

单条 SQL 语句的原子性

插入一条测试语句,然后再查询一下结果。

这里提到了自动提交事务,这时 T-SQL 默认的事务方式,它是一种能够自动执行并能够自动回滚事务的处理方式。SQL Server 除了自动提交事务之外,还有显示事务和隐式事务,暂时不在这篇文章中讨论它们的区别了。

上面的两个自动提交事务中,每一个自动提交事务只包含一条 SQL 语句,不能再分,要么成功,要么失败。

再比如,在一条 SQL 语句中插入多条数据时,其中一条数据是符合约束的。但因为另外一条数据违反了检查约束,这样也会导致整个 Insert 语句失败,因此没有一条数据能够插入到数据表中。

多条 SQL 语句形成的一个整体的原子性

假设下面的这两条 Insert 语句构成一个具备原子性特征的逻辑单元,是一个整体需要形成一个事务,那么应该如何处理。

INSERT INTO dbo.Account VALUES(1004,-1)
INSERT INTO dbo.Account VALUES(1005,500)

   

很显然如果直接这么执行的话,1004 插入失败,1005 可以插入成功,这样就是两个不同的事务了。SQL Server 提供了两种方式来确保这种包含多组 SQL 语句的逻辑块具备原子性特征。

方式一 – 使用显示事务组合多条 SQL 语句构成一个整体以实现事务的原子性

第一种就是非常常见的显示事务,通过显示的使用 BEGIN TRANSACTION, COMMIT TRANSACTION 以及 ROLLBACK TRANSACTION 命令将一组 SQL 语句形成一个完整的事务来提交,提交要么成功,要么失败。

复制代码      

-- 开始一个事务
BEGIN TRANSACTION

-- TRY CATCH 语句
BEGIN TRY

 -- 这一条会违反检查约束,插入失败
    INSERT INTO dbo.Account VALUES(1004,-1)
 -- 这一条会插入成功,但此时事务还未真正提交
    INSERT INTO dbo.Account VALUES(1005,500)

END TRY
BEGIN CATCH
 -- 发生错误,事务回滚
    IF @@TRANCOUNT > 0
        ROLLBACK TRANSACTION;
END CATCH;

-- 没有进入 CATCH 块,提交事务
IF @@TRANCOUNT > 0
    COMMIT TRANSACTION;
GO

     

复制代码      

当然最终的结果就是事务回滚,一条数据都没有插入到数据表中,所以失败时就全部失败,确保了事务的原子性。

方式二 – 通过设置  XACT_ABORT 为 ON 来确保事务的原子性

先来看默认的设置,当  XACT_ABORT 为 OFF 状态的时候。

复制代码      

-- SET XACT_ABORT OFF - 默认的 SQL Server 设置
SET XACT_ABORT OFF
BEGIN TRANSACTION
 -- 这一条会违反检查约束,插入失败
    INSERT INTO dbo.Account VALUES(1004,-1)
 -- 这一条会插入成功
 INSERT INTO dbo.Account VALUES(1005,500)
COMMIT TRANSACTION

     

复制代码      

当  XACT_ABORT 为 OFF 状态即 SQL Server 默认设置下,上面的事务中,SQL Server 在通常情况下只会回滚执行失败的语句,也就是说只会回滚 1004 这条数据,而 1005 会插入成功。很显然,这违背了事务的原子性,因为我们也没有显示的写出要 ROLLBACK TRANSACTION 来。

OK!那我们将 XACT_ABORT 设置为 ON,这时就告诉了它后面的事务,如果遇到错误就立即终止事务并回滚。这样不通过显示的 ROLLBACK TRANSACTION 也可以确保事务的原子性。

在上面的这个例子中,只有事务 2 会成功提交,而事务1和3会回滚,插入操作执行失败。

注意一点,上面的每个事务后面加了一个 GO 关键字,如果不加 GO 这个关键字,一起执行这些 SQL 语句会导致事务2和3因为事务1的执行失败而不能执行到, GO 关键字形成了一个批处理,表示前面的一组 SQL 语句一起处理。

GO 关键字非常有意思,GO 后面可以加上次数,表示前面的一条或者一组 SQL 执行几次。

通过上面的示例,应该可以理解原子性与事务的关系了,以及如何实现事务的原子性。


事务中常见的问题

了解完事务的 ACID 的原则后,再来看看在 SQL Server 中多用户并发的情况下,使用事务可能会遇到的一些情况:

脏读 (Dirty Reads) : 一 个事务正在访问并修改数据库中的数据但是没有提交,但是另外一个事务可能读取到这些已作出修改但未提交的数据。这样可能导致的结果就是所有的操作都有可能 回滚,比如第一个事务对数据做出的修改可能违背了数据表的某些约束,破坏了完整性,但是恰巧第二个事务却读取到了这些不正确的数据造成它自身操作也发生失 败回滚。

不可重复读取(Non-Repeatable Reads):  A 事务两次读取同一数据,B事务也读取这同一数据,但是 A 事务在第二次读取前B事务已经更新了这一数据。所以对于A事务来说,它第一次和第二次读取到的这一数据可能就不一致了。

幻读(Phantom Reads): 与不可重复读有点类似,都是两次读取,不同的是 A 事务第一次操作的比如说是全表的数据,此时 B 事务并不是只修改某一具体数据而是插入了一条新数据,而后 A 事务第二次读取这全表的时候就发现比上一次多了一条数据,发生幻觉了。

更新丢失(Lost Update): 两个事务同时更新,但由于某一个事务更新失败发生回滚操作,这样有可能的结果就是第二个事务已更新的数据因为第一个事务发生回滚而导致数据最终没有发生更新,因此两个事务的更新都失败了。


SQL Server 中事务的隔离级别以及与脏读,不可重复读,幻读等关系(代码论证和时间序)

了 解了在并发访问数据库的情况下可能会出现这些问题,就可以继续了解数据库隔离级别这样的一个概念,通俗一点讲就是:你希望通过何种方式让并发的事务隔离开 来,隔离到什么程度?比如可以容忍脏读,或者不希望并发的事务出现脏读的情况,那么这些可以通过隔离级别的设置使得并发事务之间的隔离程度变得宽松或者很 严峻。

隔离级别越高,读取脏数据或者造成数据不统一不完整的机会就越少,但是在高并发的系统中,性能降低就越严重。隔离级别越低,并发系统中性能上提升很大,但是数据本身可能不完整。

在 SQL Server 2012 中可以通过这样的语法来设置事务的隔离级别 (从低到高排列):

复制代码      

SET TRANSACTION ISOLATION LEVEL
    { READ UNCOMMITTED
    | READ COMMITTED
    | REPEATABLE READ
    | SNAPSHOT
    | SERIALIZABLE
    }
[ ; ]

     

复制代码      

下面通过代码示例来演示各个事务隔离级别的表现,运行下面 SQL 语句,插入一条测试语句。

复制代码      

TRUNCATE TABLE BIWORK_SSIS.dbo.Account
GO

INSERT INTO BIWORK_SSIS.dbo.Account VALUES(1001,1000)

SELECT * FROM BIWORK_SSIS.dbo.Account
GO

     

复制代码      

Read Uncommitted (未提交读)

隔离级别最低,容易产生的问题就是脏读,因为可以读取其它事务修改了的但是没有提交的数据。它的作用跟在事务中 SELECT 语句对象表上设置 (NOLOCK) 相同。

打开两个查询窗口,第一个窗口表示事务 A, 第二个窗口表示事务B。 事务A 保持默认的隔离级别,事务B 设置它们的隔离级别为 READ UNCOMMITTED, 可以通过 DBCC USEROPITIONS 查看更改后的结果。

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED 
DBCC USEROPTIONS

   

测试步骤:

先执行事务 A 的 SQL 代码

复制代码      

BEGIN TRANSACTION

UPDATE BIWORK_SSIS.dbo.Account
SET AccountBalance = 500 
WHERE ID  = 1001

WAITFOR DELAY '00:00:10'

ROLLBACK TRANSACTION

SELECT * FROM BIWORK_SSIS.dbo.Account WHERE ID = 1001

     

复制代码      

马上接着再执行 事务 B 的 SQL 代码

复制代码      

-- 第1次查询 发生在 A 事务未提交或者回滚之前
SELECT * FROM BIWORK_SSIS.dbo.Account WHERE ID = 1001

WAITFOR DELAY '00:00:10'

-- 第2次查询 发生在 A 事务回滚之后
SELECT * FROM BIWORK_SSIS.dbo.Account WHERE ID = 1001

     

复制代码      

可以看出,事务 B 对 ID = 1001 的这条数据进行了两次读取,但是很显然第一次读取的数据是脏数据。下面模拟了一下它们发生的时序,虽然不算严谨,但是可以帮助理解脏读产生的原因。

还 可以把事务B 的隔离级别改回来成为默认的  READ COMMITTED,然后运行完事务 A 之后马上运行带有 NOLOCK 的查询,效果和上面描述的也是一致的。 一旦加上 NOLOCK,可以认为它的作用就等同于隔离级别为 READ UNCOMMITTED。

SELECT * FROM BIWORK_SSIS.dbo.Account WITH(NOLOCK) WHERE ID = 1001

   

 

Read Committed (已提交读)

这 是 SQL Server 的默认设置,已提交读,可以避免脏读,可以满足大多数要求。事务中的语句不能读取已由其它事务做出修改但是还未提交的数据,但是能够读取由其它事务做出修 改并提交了的数据。也就是说,有可能会出现 Non-Repeatable Reads 不可重复读取和 Phantom Reads 幻读的情况,因为当前事务中可能出现两次读取同一资源,但是两次读取的过程之间,另外一事务可能对这一资源完成了读取更新并提交的行为,这样数据前后可能 就不一致了。因此,这一个默认的隔离级别能够解决脏读但是解决不了 Non-Repeatable Reads 不可重复读。

接着上一个例子,看看如果将隔离级别设置为 READ COMMITTED,能否避免脏读? 还是先运行事务 A,再接着运行事务 B。

因为已提交读不能读取已由其它事物做出修改但是还未提交的数据,因此事务B 就必须等待事务 A 完成对数据的修改提交或者回滚之后才能开始读取。运行事务A 和事务B,明显事务B 有一个等待事务A提交或者回滚的过程,看看它们的时序图。

由此可以看出隔离级别 READ COMMITTED 可以避免脏读,但是也有可能出现其它的问题,请看这个例子。先执行事务A,接着直接执行事务 B。

从 上面的执行结果来看,很明显在事务 A 中,同一个事务中对 ID  = 1001 的取值出现了前后不一致的情况。假设这里不是简单的查询,而是先查询账户余额有 1000元钱,然后后面的动作就是取 1000元钱,很明显第二次取的时候发现只有 500 元了。原因就是在第一次查询和取的间隙之间被事务 B 钻了空子,修改了余额。这种情况就是上面所介绍到的不可重复读取,请看下面的时序图。

所以 READ COMMITTED 已提交读隔离级别能够避免脏读,但是仍然会遇到不可重复读取的问题。

Repeatable Read (可重复读)

不 能读取已由其它事务修改了但是未提交的行,其它任何事务也不能修改在当前事务完成之前由当前事务读取的数据。但是对于其它事务插入的新行数据,当前事务第 二次访问表行时会检索这一新行。因此,这一个隔离级别的设置解决了 Non-Repeatable Reads 不可重复读取的问题,但是避免不了 Phantom Reads 幻读。

接着上面的例子做出一些修改,增加了一些查询,记得把 ID = 1001 的余额改回 1000。将事务 A 的隔离级别设置为 REPEATABLE READ 可重复读级别,来看看这个隔离级别的表现。

尽管在最后的查询结果中, ID  = 1001 的余额为 500 元,但是在事务 A 中的两次读取一次发生在 事务 B 开始之前,一次发生在 事务 B 提交之后,但是它们读取的余额是保持一致的,看不到事务 B 对这个值的修改。

从 上面的时序图中可以看出,事务 A 第一次读取到的 ID = 1001 的余额值和第二次读取到的是一样的,可以理解为在事务 A 的查询期间是不允许事务 B 修改这个值的。 因为事务 A 确实没有看到这个变化,所以事务A 也确实认为事务B 听了它的话,没有做出 Update 的操作。但是实际上,事务 B 已经完成了这个操作,只不过由于 事务 A 中隔离级别设置为 REPEATABLE READ 可重复读,所以两次读取的结果始终保持着一致。

那么这里的示例是事务B在修改数据,如果是新增加一行记录呢?

事务 A 又开始晕菜了!居然两次查询的结果不一样,第二次查询多了一条数据,这就是幻读!


SNAPSHOT (快照隔离)

可 以解决幻读 Phantom Reads 的问题,当前事务中读取的数据在整个事务开始到事务提交结束之间,这个数据版本是一致的。其它的事务可能对这些数据做出修改,但是对于当前事务来说它是看 不到这些变化。有点类似于当前事务拿到这个数据的时候是拿到这个数据的快照,因此在这个快照上做出的操作同一事务中前后几次操作都是基于同一数据版本。因 此,这一个隔离级别的设置可以解决 Phantom Reads 幻读问题。但是要注意的是,其它事务是可以在当前事务完成之前修改由当前事务读取的数据。

在使用 SNAPSHOT 之前要注意,默认情况下数据库不允许设置 SNAPSHOT 隔离级别,直接设置会出现类似于这样的错误:

DBCC execution completed. If DBCC printed error messages, contact your system administrator.

Msg 3952, Level 16, State 1, Line 8

Snapshot isolation transaction failed accessing database ‘BIWORK_SSIS’ because snapshot isolation is not allowed in this database. Use ALTER DATABASE to allow snapshot isolation.

所以要使用 SET 命令开启这个支持

ALTER DATABASE BIWORK_SSIS
SET ALLOW_SNAPSHOT_ISOLATION ON

   

并且在开始前先清空其它的 ID,只保留 ID = 1001 的这条记录。

DELETE FROM BIWORK_SSIS.dbo.Account
WHERE ID <> 1001

   

这样通过设置隔离级别是 SNAPSHOT就解决了幻读的问题,保证了在事务 A 中查询的数据行版本是前后一致的。

但 是大家发现没有?无论在事务 A 中使用 Repeatable Read 还是 Snapshot 仍然不可避免的阻止事务B 对共享的资源做出了修改,尽管这个修改没有被事务 A 发现,事务 A 中的数据还是保持了一致,但是实际上还是做出了修改。只要事务 A 一提交结束,马上就可以看到事务 B 做出的这些修改已经生效了。回顾之前提到的,如果我第一次查询有1000元,第二次动作可能就是取1000元。在这两次动作之间另外的一个事务对金额做出 了修改,尽管我两次读取都是1000元,但是实际上是不符合常理的。要么,我先查询然后再取款这个动作是连贯的,然后另外一个事务再对金额做出修改。要 么,其它事务先对金额做出修改,比如扣去500元,那么我再查询再取款这个钱数还是一致的。也就是说,在事务 A 对某一个资源做出操作的时候,形成了独占,事务 B 进不来。或者事务 B 在对这个资源做操作的时候,事务 A 也必须等待事务 B 结束后才能开始它的事务,那么这里就要使用到最严格的隔离级别了 – SERIALIZABLE。

 

SERIALIZABLE(序列化)

性 能最低,隔离级别最高最严格,可以几乎上面提到的所有问题。比如不能读取其它已由其它事务修改但是没有提交的数据,不允许其它事务在当前事务完成修改之前 修改由当前事务读取的数据,不允许其它事务在当前事务完成修改之前插入新的行。它的作用与在事务内所有 SELECT 语句中的所有表上设置 HOLDLOCK 相同,并发级别比较低但又对安全性要求比较高的时候可以考虑使用。如果并发级别很高,使用这个隔离级别,性能瓶颈将非常严重。

将事务 A 的隔离级别调整成 SERIALIZABLE,然后执行 A 然后再执行 B。

在 这里可以看到事务B 的执行基本上是在事务A提交之后才开始的,当事务 A 在执行的时候,事务 B 因为也要访问这个资源所以一直阻塞在那里直到事务 A 提交。 并不是说事务 B 没有开始,而是说在执行 SELECT 查询的时候因为事务 A 占用了这个资源,所以处于等待状态。

在 SQL Server 中设置隔离级别要注意:一次只能设置一个隔离级别的选项,并且设置的隔离级别对当前连接一直有效直到显式修改为止。事务中执行的所有读取操作也都会在指定 的隔离级别规则下运行,除非在 SELECT 操作语句中对表指定了其它的锁或者版本控制行为。

注:上面的时序图只是用来帮助理解事务的隔离级别,只是一个大概的执行顺序,当然也跟我执行事务 A 和 事务 B 的时间点相关,所以并不能真正反映实际过程中 SQL 语句提交和执行的实际顺序,真正提交的过程可以通过 SQL Profiler 去跟踪看看。

 

















About Me

...............................................................................................................................

● 本文整理自网络

● 本文在itpub(http://blog.itpub.net/26736162)、博客园(http://www.cnblogs.com/lhrbest)和个人微信公众号(xiaomaimiaolhr)上有同步更新

● 本文itpub地址:http://blog.itpub.net/26736162/abstract/1/

● 本文博客园地址:http://www.cnblogs.com/lhrbest

● 本文pdf版及小麦苗云盘地址:http://blog.itpub.net/26736162/viewspace-1624453/

● 数据库笔试面试题库及解答:http://blog.itpub.net/26736162/viewspace-2134706/

● QQ群:230161599     微信群:私聊

● 联系我请加QQ好友(646634621),注明添加缘由

● 于 2017-07-01 09:00 ~ 2017-07-31 22:00 在魔都完成

● 文章内容来源于小麦苗的学习笔记,部分整理自网络,若有侵权或不当之处还请谅解

● 版权所有,欢迎分享本文,转载请保留出处

...............................................................................................................................

拿起手机使用微信客户端扫描下边的左边图片来关注小麦苗的微信公众号:xiaomaimiaolhr,扫描右边的二维码加入小麦苗的QQ群,学习最实用的数据库技术。

ico_mailme_02.png
DBA笔试面试讲解
欢迎与我联系


来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/26736162/viewspace-2142403/,如需转载,请注明出处,否则将追究法律责任。

原 本打算写有关 SSIS Package 中的事务控制过程的,但是发现很多基本的概念还是需要有 SQL Server 事务和事务的隔离级别做基础铺垫。所以花了点时间,把 SQL Server 数据库中的事务概念,ACID 原则,事务中常见的问题,问题造成的原因和事务隔离级别等这些方面的知识好好的整理了一下。

其实有关 SQL Server 中的事务,说实话因为内容太多, 话题太广,稍微力度控制不好就超过了我目前知识能力范围,就不是三言两语能够讲清楚的。所以希望大家能够指出其中总结的不足之处,对我来说多了提高的机会,更可以帮助大家加深对事务的理解。


本文涉及到的知识点:

  • SQL Server 数据库中事务的概念
  • ACID 原则 (加了一部分内容专门解释原子性,提到了显示事务以及 XACT_ABORT 机制来确保事务的原子性)
  • 列出事务中常见的问题以及原因:脏读,未提交读,不可重复读,幻读 等
  • SQL Server中 事务的隔离级别以及它们如何做到避免脏读,未提交读,不可重复读和幻读 (用代码描述了这些问题,并且使用时间序来解释产生的原因)

SQL Server 数据库中事务的概念

数据库中的事务是数据库并发控制的基本单位,一条或者一组语句要么全部成功,对数据库中的某些数据成功修改; 要么全部不成功,数据库中的数据还原到这些语句执行

之 前的样子。比如网上订火车票,要么你定票成功,余票显示就减一张; 要么你定票失败获取取消订票,余票的数量还是那么多。不允许出现你订票成功了,余票没有减少或者你取消订票了,余票显示却少了一张的这种情况。这种不被允 许出现的情况就要求购票和余票减少这两个不同的操作必须放在一起,成为一个完整的逻辑链,这样就构成了一个事务。


数据库中事务的 ACID 原则

原子性 (Atomictiy):事务的原子性是指一个事务中包含的一条语句或者多条语句构成了一个完整的逻辑单元,这个逻辑单元具有不可再分的原子性。这个逻辑单元要么一起提交执行全部成功,要么一起提交执行全部失败。

一致性 (Consistency):可以理解为数据的完整性,事务的提交要确保在数据库上的操作没有破坏数据的完整性,比如说不要违背一些约束的数据插入或者修改行为。一旦破坏了数据的完整性,SQL Server 会回滚这个事务来确保数据库中的数据是一致的。

隔离性(Isolation):与数据库中的事务隔离级别以及锁相关,多个用户可以对同一数据并发访问而又不破坏数据的正确性和完整性。但是,并行事务的修改必须与其它并行事务的修改相互独立,隔离。 但是在不同的隔离级别下,事务的读取操作可能得到的结果是不同的。

持久性(Durability):数据持久化,事务一旦对数据的操作完成并提交后,数据修改就已经完成,即使服务重启这些数据也不会改变。相反,如果在事务的执行过程中,系统服务崩溃或者重启,那么事务所有的操作就会被回滚,即回到事务操作之前的状态。

我 理解在极端断电或者系统崩溃的情况下,一个发生在事务未提交之前,数据库应该记录了这个事务的”ID”和部分已经在数据库上更新的数据。供电恢复数据库重 新启动之后,这时完成全部撤销和回滚操作。如果在事务提交之后的断电,有可能更改的结果没有正常写入磁盘持久化,但是有可能丢失的数据会通过事务日志自动 恢复并重新生成以写入磁盘完成持久化。

原子性的进一步理解

关于原子性,有必要在这里多补充一下,因为我们描述的概念是指在事务中的原子性。一条 SQL 语句和多条 SQL 语句在处理原子性上是有一些区别的,下面演示了这些区别。

先运行这些代码,创建一个非常简单的测试表,这张表只简单模拟了一个账户的 ID 和账户余额。

复制代码      

USE BIWORK_SSIS
GO

IF OBJECT_ID('dbo.Account') IS NOT NULL
DROP TABLE dbo.Account
GO

CREATE TABLE dbo.Account
(
  ID INT PRIMARY KEY,
  AccountBalance MONEY CHECK(AccountBalance >= 0)
)

     

复制代码      

单条 SQL 语句的原子性

插入一条测试语句,然后再查询一下结果。

这里提到了自动提交事务,这时 T-SQL 默认的事务方式,它是一种能够自动执行并能够自动回滚事务的处理方式。SQL Server 除了自动提交事务之外,还有显示事务和隐式事务,暂时不在这篇文章中讨论它们的区别了。

上面的两个自动提交事务中,每一个自动提交事务只包含一条 SQL 语句,不能再分,要么成功,要么失败。

再比如,在一条 SQL 语句中插入多条数据时,其中一条数据是符合约束的。但因为另外一条数据违反了检查约束,这样也会导致整个 Insert 语句失败,因此没有一条数据能够插入到数据表中。

多条 SQL 语句形成的一个整体的原子性

假设下面的这两条 Insert 语句构成一个具备原子性特征的逻辑单元,是一个整体需要形成一个事务,那么应该如何处理。

INSERT INTO dbo.Account VALUES(1004,-1)
INSERT INTO dbo.Account VALUES(1005,500)

   

很显然如果直接这么执行的话,1004 插入失败,1005 可以插入成功,这样就是两个不同的事务了。SQL Server 提供了两种方式来确保这种包含多组 SQL 语句的逻辑块具备原子性特征。

方式一 – 使用显示事务组合多条 SQL 语句构成一个整体以实现事务的原子性

第一种就是非常常见的显示事务,通过显示的使用 BEGIN TRANSACTION, COMMIT TRANSACTION 以及 ROLLBACK TRANSACTION 命令将一组 SQL 语句形成一个完整的事务来提交,提交要么成功,要么失败。

复制代码      

-- 开始一个事务
BEGIN TRANSACTION

-- TRY CATCH 语句
BEGIN TRY

 -- 这一条会违反检查约束,插入失败
    INSERT INTO dbo.Account VALUES(1004,-1)
 -- 这一条会插入成功,但此时事务还未真正提交
    INSERT INTO dbo.Account VALUES(1005,500)

END TRY
BEGIN CATCH
 -- 发生错误,事务回滚
    IF @@TRANCOUNT > 0
        ROLLBACK TRANSACTION;
END CATCH;

-- 没有进入 CATCH 块,提交事务
IF @@TRANCOUNT > 0
    COMMIT TRANSACTION;
GO

     

复制代码      

当然最终的结果就是事务回滚,一条数据都没有插入到数据表中,所以失败时就全部失败,确保了事务的原子性。

方式二 – 通过设置  XACT_ABORT 为 ON 来确保事务的原子性

先来看默认的设置,当  XACT_ABORT 为 OFF 状态的时候。

复制代码      

-- SET XACT_ABORT OFF - 默认的 SQL Server 设置
SET XACT_ABORT OFF
BEGIN TRANSACTION
 -- 这一条会违反检查约束,插入失败
    INSERT INTO dbo.Account VALUES(1004,-1)
 -- 这一条会插入成功
 INSERT INTO dbo.Account VALUES(1005,500)
COMMIT TRANSACTION

     

复制代码      

当  XACT_ABORT 为 OFF 状态即 SQL Server 默认设置下,上面的事务中,SQL Server 在通常情况下只会回滚执行失败的语句,也就是说只会回滚 1004 这条数据,而 1005 会插入成功。很显然,这违背了事务的原子性,因为我们也没有显示的写出要 ROLLBACK TRANSACTION 来。

OK!那我们将 XACT_ABORT 设置为 ON,这时就告诉了它后面的事务,如果遇到错误就立即终止事务并回滚。这样不通过显示的 ROLLBACK TRANSACTION 也可以确保事务的原子性。

在上面的这个例子中,只有事务 2 会成功提交,而事务1和3会回滚,插入操作执行失败。

注意一点,上面的每个事务后面加了一个 GO 关键字,如果不加 GO 这个关键字,一起执行这些 SQL 语句会导致事务2和3因为事务1的执行失败而不能执行到, GO 关键字形成了一个批处理,表示前面的一组 SQL 语句一起处理。

GO 关键字非常有意思,GO 后面可以加上次数,表示前面的一条或者一组 SQL 执行几次。

通过上面的示例,应该可以理解原子性与事务的关系了,以及如何实现事务的原子性。


事务中常见的问题

了解完事务的 ACID 的原则后,再来看看在 SQL Server 中多用户并发的情况下,使用事务可能会遇到的一些情况:

脏读 (Dirty Reads) : 一 个事务正在访问并修改数据库中的数据但是没有提交,但是另外一个事务可能读取到这些已作出修改但未提交的数据。这样可能导致的结果就是所有的操作都有可能 回滚,比如第一个事务对数据做出的修改可能违背了数据表的某些约束,破坏了完整性,但是恰巧第二个事务却读取到了这些不正确的数据造成它自身操作也发生失 败回滚。

不可重复读取(Non-Repeatable Reads):  A 事务两次读取同一数据,B事务也读取这同一数据,但是 A 事务在第二次读取前B事务已经更新了这一数据。所以对于A事务来说,它第一次和第二次读取到的这一数据可能就不一致了。

幻读(Phantom Reads): 与不可重复读有点类似,都是两次读取,不同的是 A 事务第一次操作的比如说是全表的数据,此时 B 事务并不是只修改某一具体数据而是插入了一条新数据,而后 A 事务第二次读取这全表的时候就发现比上一次多了一条数据,发生幻觉了。

更新丢失(Lost Update): 两个事务同时更新,但由于某一个事务更新失败发生回滚操作,这样有可能的结果就是第二个事务已更新的数据因为第一个事务发生回滚而导致数据最终没有发生更新,因此两个事务的更新都失败了。


SQL Server 中事务的隔离级别以及与脏读,不可重复读,幻读等关系(代码论证和时间序)

了 解了在并发访问数据库的情况下可能会出现这些问题,就可以继续了解数据库隔离级别这样的一个概念,通俗一点讲就是:你希望通过何种方式让并发的事务隔离开 来,隔离到什么程度?比如可以容忍脏读,或者不希望并发的事务出现脏读的情况,那么这些可以通过隔离级别的设置使得并发事务之间的隔离程度变得宽松或者很 严峻。

隔离级别越高,读取脏数据或者造成数据不统一不完整的机会就越少,但是在高并发的系统中,性能降低就越严重。隔离级别越低,并发系统中性能上提升很大,但是数据本身可能不完整。

在 SQL Server 2012 中可以通过这样的语法来设置事务的隔离级别 (从低到高排列):

复制代码      

SET TRANSACTION ISOLATION LEVEL
    { READ UNCOMMITTED
    | READ COMMITTED
    | REPEATABLE READ
    | SNAPSHOT
    | SERIALIZABLE
    }
[ ; ]

     

复制代码      

下面通过代码示例来演示各个事务隔离级别的表现,运行下面 SQL 语句,插入一条测试语句。

复制代码      

TRUNCATE TABLE BIWORK_SSIS.dbo.Account
GO

INSERT INTO BIWORK_SSIS.dbo.Account VALUES(1001,1000)

SELECT * FROM BIWORK_SSIS.dbo.Account
GO

     

复制代码      

Read Uncommitted (未提交读)

隔离级别最低,容易产生的问题就是脏读,因为可以读取其它事务修改了的但是没有提交的数据。它的作用跟在事务中 SELECT 语句对象表上设置 (NOLOCK) 相同。

打开两个查询窗口,第一个窗口表示事务 A, 第二个窗口表示事务B。 事务A 保持默认的隔离级别,事务B 设置它们的隔离级别为 READ UNCOMMITTED, 可以通过 DBCC USEROPITIONS 查看更改后的结果。

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED 
DBCC USEROPTIONS

   

测试步骤:

先执行事务 A 的 SQL 代码

复制代码      

BEGIN TRANSACTION

UPDATE BIWORK_SSIS.dbo.Account
SET AccountBalance = 500 
WHERE ID  = 1001

WAITFOR DELAY '00:00:10'

ROLLBACK TRANSACTION

SELECT * FROM BIWORK_SSIS.dbo.Account WHERE ID = 1001

     

复制代码      

马上接着再执行 事务 B 的 SQL 代码

复制代码      

-- 第1次查询 发生在 A 事务未提交或者回滚之前
SELECT * FROM BIWORK_SSIS.dbo.Account WHERE ID = 1001

WAITFOR DELAY '00:00:10'

-- 第2次查询 发生在 A 事务回滚之后
SELECT * FROM BIWORK_SSIS.dbo.Account WHERE ID = 1001

     

复制代码      

可以看出,事务 B 对 ID = 1001 的这条数据进行了两次读取,但是很显然第一次读取的数据是脏数据。下面模拟了一下它们发生的时序,虽然不算严谨,但是可以帮助理解脏读产生的原因。

还 可以把事务B 的隔离级别改回来成为默认的  READ COMMITTED,然后运行完事务 A 之后马上运行带有 NOLOCK 的查询,效果和上面描述的也是一致的。 一旦加上 NOLOCK,可以认为它的作用就等同于隔离级别为 READ UNCOMMITTED。

SELECT * FROM BIWORK_SSIS.dbo.Account WITH(NOLOCK) WHERE ID = 1001

   

 

Read Committed (已提交读)

这 是 SQL Server 的默认设置,已提交读,可以避免脏读,可以满足大多数要求。事务中的语句不能读取已由其它事务做出修改但是还未提交的数据,但是能够读取由其它事务做出修 改并提交了的数据。也就是说,有可能会出现 Non-Repeatable Reads 不可重复读取和 Phantom Reads 幻读的情况,因为当前事务中可能出现两次读取同一资源,但是两次读取的过程之间,另外一事务可能对这一资源完成了读取更新并提交的行为,这样数据前后可能 就不一致了。因此,这一个默认的隔离级别能够解决脏读但是解决不了 Non-Repeatable Reads 不可重复读。

接着上一个例子,看看如果将隔离级别设置为 READ COMMITTED,能否避免脏读? 还是先运行事务 A,再接着运行事务 B。

因为已提交读不能读取已由其它事物做出修改但是还未提交的数据,因此事务B 就必须等待事务 A 完成对数据的修改提交或者回滚之后才能开始读取。运行事务A 和事务B,明显事务B 有一个等待事务A提交或者回滚的过程,看看它们的时序图。

由此可以看出隔离级别 READ COMMITTED 可以避免脏读,但是也有可能出现其它的问题,请看这个例子。先执行事务A,接着直接执行事务 B。

从 上面的执行结果来看,很明显在事务 A 中,同一个事务中对 ID  = 1001 的取值出现了前后不一致的情况。假设这里不是简单的查询,而是先查询账户余额有 1000元钱,然后后面的动作就是取 1000元钱,很明显第二次取的时候发现只有 500 元了。原因就是在第一次查询和取的间隙之间被事务 B 钻了空子,修改了余额。这种情况就是上面所介绍到的不可重复读取,请看下面的时序图。

所以 READ COMMITTED 已提交读隔离级别能够避免脏读,但是仍然会遇到不可重复读取的问题。

Repeatable Read (可重复读)

不 能读取已由其它事务修改了但是未提交的行,其它任何事务也不能修改在当前事务完成之前由当前事务读取的数据。但是对于其它事务插入的新行数据,当前事务第 二次访问表行时会检索这一新行。因此,这一个隔离级别的设置解决了 Non-Repeatable Reads 不可重复读取的问题,但是避免不了 Phantom Reads 幻读。

接着上面的例子做出一些修改,增加了一些查询,记得把 ID = 1001 的余额改回 1000。将事务 A 的隔离级别设置为 REPEATABLE READ 可重复读级别,来看看这个隔离级别的表现。

尽管在最后的查询结果中, ID  = 1001 的余额为 500 元,但是在事务 A 中的两次读取一次发生在 事务 B 开始之前,一次发生在 事务 B 提交之后,但是它们读取的余额是保持一致的,看不到事务 B 对这个值的修改。

从 上面的时序图中可以看出,事务 A 第一次读取到的 ID = 1001 的余额值和第二次读取到的是一样的,可以理解为在事务 A 的查询期间是不允许事务 B 修改这个值的。 因为事务 A 确实没有看到这个变化,所以事务A 也确实认为事务B 听了它的话,没有做出 Update 的操作。但是实际上,事务 B 已经完成了这个操作,只不过由于 事务 A 中隔离级别设置为 REPEATABLE READ 可重复读,所以两次读取的结果始终保持着一致。

那么这里的示例是事务B在修改数据,如果是新增加一行记录呢?

事务 A 又开始晕菜了!居然两次查询的结果不一样,第二次查询多了一条数据,这就是幻读!


SNAPSHOT (快照隔离)

可 以解决幻读 Phantom Reads 的问题,当前事务中读取的数据在整个事务开始到事务提交结束之间,这个数据版本是一致的。其它的事务可能对这些数据做出修改,但是对于当前事务来说它是看 不到这些变化。有点类似于当前事务拿到这个数据的时候是拿到这个数据的快照,因此在这个快照上做出的操作同一事务中前后几次操作都是基于同一数据版本。因 此,这一个隔离级别的设置可以解决 Phantom Reads 幻读问题。但是要注意的是,其它事务是可以在当前事务完成之前修改由当前事务读取的数据。

在使用 SNAPSHOT 之前要注意,默认情况下数据库不允许设置 SNAPSHOT 隔离级别,直接设置会出现类似于这样的错误:

DBCC execution completed. If DBCC printed error messages, contact your system administrator.

Msg 3952, Level 16, State 1, Line 8

Snapshot isolation transaction failed accessing database ‘BIWORK_SSIS’ because snapshot isolation is not allowed in this database. Use ALTER DATABASE to allow snapshot isolation.

所以要使用 SET 命令开启这个支持

ALTER DATABASE BIWORK_SSIS
SET ALLOW_SNAPSHOT_ISOLATION ON

   

并且在开始前先清空其它的 ID,只保留 ID = 1001 的这条记录。

DELETE FROM BIWORK_SSIS.dbo.Account
WHERE ID <> 1001

   

这样通过设置隔离级别是 SNAPSHOT就解决了幻读的问题,保证了在事务 A 中查询的数据行版本是前后一致的。

但 是大家发现没有?无论在事务 A 中使用 Repeatable Read 还是 Snapshot 仍然不可避免的阻止事务B 对共享的资源做出了修改,尽管这个修改没有被事务 A 发现,事务 A 中的数据还是保持了一致,但是实际上还是做出了修改。只要事务 A 一提交结束,马上就可以看到事务 B 做出的这些修改已经生效了。回顾之前提到的,如果我第一次查询有1000元,第二次动作可能就是取1000元。在这两次动作之间另外的一个事务对金额做出 了修改,尽管我两次读取都是1000元,但是实际上是不符合常理的。要么,我先查询然后再取款这个动作是连贯的,然后另外一个事务再对金额做出修改。要 么,其它事务先对金额做出修改,比如扣去500元,那么我再查询再取款这个钱数还是一致的。也就是说,在事务 A 对某一个资源做出操作的时候,形成了独占,事务 B 进不来。或者事务 B 在对这个资源做操作的时候,事务 A 也必须等待事务 B 结束后才能开始它的事务,那么这里就要使用到最严格的隔离级别了 – SERIALIZABLE。

 

SERIALIZABLE(序列化)

性 能最低,隔离级别最高最严格,可以几乎上面提到的所有问题。比如不能读取其它已由其它事务修改但是没有提交的数据,不允许其它事务在当前事务完成修改之前 修改由当前事务读取的数据,不允许其它事务在当前事务完成修改之前插入新的行。它的作用与在事务内所有 SELECT 语句中的所有表上设置 HOLDLOCK 相同,并发级别比较低但又对安全性要求比较高的时候可以考虑使用。如果并发级别很高,使用这个隔离级别,性能瓶颈将非常严重。

将事务 A 的隔离级别调整成 SERIALIZABLE,然后执行 A 然后再执行 B。

在 这里可以看到事务B 的执行基本上是在事务A提交之后才开始的,当事务 A 在执行的时候,事务 B 因为也要访问这个资源所以一直阻塞在那里直到事务 A 提交。 并不是说事务 B 没有开始,而是说在执行 SELECT 查询的时候因为事务 A 占用了这个资源,所以处于等待状态。

在 SQL Server 中设置隔离级别要注意:一次只能设置一个隔离级别的选项,并且设置的隔离级别对当前连接一直有效直到显式修改为止。事务中执行的所有读取操作也都会在指定 的隔离级别规则下运行,除非在 SELECT 操作语句中对表指定了其它的锁或者版本控制行为。

注:上面的时序图只是用来帮助理解事务的隔离级别,只是一个大概的执行顺序,当然也跟我执行事务 A 和 事务 B 的时间点相关,所以并不能真正反映实际过程中 SQL 语句提交和执行的实际顺序,真正提交的过程可以通过 SQL Profiler 去跟踪看看。

 

这篇关于SQL Server事务隔离级别详解的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!