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

本来打算写有关 SSIS Package 中的事务控制过程的,可是发现不少基本的概念仍是须要有 SQL Server 事务和事务的隔离级别作基础铺垫。因此花了点时间,把 SQL Server 数据库中的事务概念,ACID 原则,事务中常见的问题,问题形成的缘由和事务隔离级别等这些方面的知识好好的整理了一下。数据库

其实有关 SQL Server 中的事务,说实话由于内容太多, 话题太广,稍微力度控制很差就超过了我目前知识能力范围,就不是三言两语可以讲清楚的。因此但愿你们可以指出其中总结的不足之处,对我来讲多了提升的机会,更能够帮助你们加深对事务的理解。安全


本文涉及到的知识点:并发

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

SQL Server 数据库中事务的概念高并发

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

以前的样子。好比网上订火车票,要么你定票成功,余票显示就减一张; 要么你定票失败获取取消订票,余票的数量仍是那么多。不容许出现你订票成功了,余票没有减小或者你取消订票了,余票显示却少了一张的这种状况。这种不被容许出现的状况就要求购票和余票减小这两个不一样的操做必须放在一块儿,成为一个完整的逻辑链,这样就构成了一个事务。测试


数据库中事务的 ACID 原则this

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

一致性 (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 去跟踪看看。

相关文章
相关标签/搜索