深入sql server中的事务
2. 不可重复读(nonrepeatable read)... 1
1.1. 显式事务(Explicit Transactions)... 3
1.2. 自动提交事务(Autocommit Transactions)... 4
1.3. 隐式事务(Implicit Transactions)... 4
一. 概述
当多个用户同时访问数据库的同一资源时,叫做并发访问。如果并发的访问中有用户对数据进行修改,很可能就会对其他访问同一资源的用户产生不利的影响。可能产生的并发不利影响有以下几类:脏读、不可重复读和幻读。
为了避免并发访问产生的不利影响,sql server设计有两种并发访问的控制机制:锁、行版本控制。
二. 并发访问的不利影响
并发访问,如果没有并发访问控制机制,可能产生的不利影响有以下几种
1. 脏读(dirty read)
如果一个用户在更新一条记录,这时第二个用户来读取这条更新了的记录,但是第一个用户在更新了记录后又反悔了,不修改了,回滚了刚才的更新。这样,导致了第二个用户实际上读取到了一条根本就没有存在过的修改后的记录。如果第一个用户在修改记录期间,把所修改的记录锁住,在修改完成前别的用户读取不到记录,就能避免这种情况。
2. 不可重复读(nonrepeatable read)
第一个用户在一次事务中读取同一记录两次,第一次读取一条记录后,又有第二个用户来访问这条记录,并修改了这条记录,第一个用户第二次读取这条记录时,得到的是跟第一次不同的数据了。如果第一个用户在两次读取之间锁住要读取的记录,别的用户不能去修改相应的记录就能避免这种情况。
3. 幻读(phantom read)
第一个用户在一次事务中两次读取同样满足条件的一批记录,第一次读取一批记录后,又有第二个用户来访问这个表,并在这个表中插入或者删除了一些记录,第一个用户第二次以同样条件读取这批记录时,可能得到的结果有些记录是在第一次读取时有,第二次的结果中没有了,或者是第二次读取的结果中有的记录在第一次读取的结果中没有的。如果第一个用户在两次读取之间锁住要读取的记录,别的用户不能去修改相应的记录,也不能增删记录,就能避免这种情况。
三. 并发访问的控制机制
Sql server中提供了两种并发控制的机制以避免在并发访问时可能产生的不利影响。这两种机制是:
1. 锁
每个事务对所依赖的资源(如行、页或表)请求不同类型的锁。锁可以阻止其他事务以某种可能会导致事务请求锁出错的方式修改资源。当事务不再依赖锁定的资源时,它将释放锁。
根据需要锁定资源的粒度和层次,锁有许多类型,主要的有几种:
表类型:锁定整个表
行类型:锁定某个行
文件类型:锁定某个数据库文件
数据库类型:锁定整个数据库
页类型:锁定8K为单位的数据库页
锁的粒度越小,锁定的范围越小,对别的访问的阻塞就越小,但是所用的锁可能会比较多,锁的消耗就比较大。锁的粒度越大,对别的访问的阻塞可能性就越大,但是所用的锁就会比较少,锁的消耗就比较小。
对于编程人员来说,不用手工去设置控制锁,sql server通过设置事务的隔离级别自动管理锁的设置和控制。
Sql server专门管理锁的是锁管理器,锁管理器通过查询分析器分析待执行的sql语句,来判断语句将会访问哪些资源,进行什么操作,然后结合设定的隔离级别自动分配管理需要用到的锁。
2. 行版本控制
当启用了基于行版本控制的隔离级别时,数据库引擎 将维护修改的每一行的版本。应用程序可以指定事务使用行版本查看事务或查询开始时存在的数据,而不是使用锁保护所有读取。通过使用行版本控制,读取操作阻止其他事务的可能性将大大降低。
四. 隔离级别
上面提到了,sql server通过设置隔离级别来控制锁的使用,从而实现并发法访问控制。
Microsoft SQL Server 数据库引擎支持所有这些隔离级别:
l 未提交读(隔离事务的最低级别,只能保证不读取物理上损坏的数据)
l 已提交读(数据库引擎的默认级别)
l 可重复读
l 可序列化(隔离事务的最高级别,事务之间完全隔离)
这几种隔离级别,对应上面三种并发访问可能产生的不利影响,分别有不同的效果,见下表:
隔离级别 |
脏读 |
不可重复读 |
幻读 |
未提交读 |
是 |
是 |
是 |
已提交读 |
否 |
是 |
是 |
可重复读 |
否 |
否 |
是 |
快照 |
否 |
否 |
否 |
可序列化 |
否 |
否 |
否 |
五. 事务
事务是一个逻辑上的单个的工作单元,其中可以包括许多操作,但是它们在逻辑上是一个整体,要么全部完成,要么全部失败,就好像什么操作都没进行似的。
事务是十分可靠坚固的机制,它能保证事务要么全部完成,要么能全部回滚。
l 锁:使用锁的机制尽可能的保证并发事务的隔离性,避免并发的不利影响。
l 事务日志:事务日志记录着整个事务的所有操作步骤,必要的时候靠日志重新开始事务或者回滚事务。不管出现什么状况,哪怕是网络中断,机器断电,甚至是数据库引擎本身出问题了,通过事务日志都能保证事务的完整性。
l 事务管理:保证一个事务的原子性和数据的一致性。一个事务开始后,它要么成功的完成,要么失败,回滚到事务没开始前的那个状态,事务开始做的所有修改都将复原。
1. 事务的模式
控制事务的开始结束的时间点和事务的范围,有几种事务模式:
1.1.显式事务(Explicit Transactions)
显式事务通过sql脚本的BEGIN TRANSACTION或者编程接口(API)的开始事务语句启动事务,以sql脚本的COMMIT 或 ROLLBACK语句提交或回滚事务,编程接口(API)的提交事务或回滚事务语句结束事务。都是通过显式的命令控制事务的开始和结束。
从事务开始到事务提交或者回滚是一个完整的事务周期,事务一旦开始,结果要么是提交,要么是回滚。
如果事务范围内发生错误,错误分为几种类型,不同类型的错误有不同的行为。
l 严重错误
比如,客户端到服务端的网络中断了,或者客户的机器被关机了,数据引擎会被通知数据连接已中断,这样严重的错误数据引擎会自动在服务端回滚整个事务。
l 运行时错误
语句之间的“GO”命令形成的区域为命令批次。数据引擎编译和执行语句是以批次为单位的。一次编译一个批次的命令,编译完成后执行这个批次的命令。存储过程是整个被一次编译的,所以一个存储过程内不分批次,整个过程就是一个批次。
大多数情况下,在一个批次中一条语句发生运行时错误,这个语句将被中止,同时同一批次的所有后续语句也不再执行,但同一批次前面已经执行的命令依然有效。但是可以使用了try…catch捕获错误,并进行相应处理,比如执行事务回滚命令。
有一些运行时错误,比如插入了一个主键重复的记录,只中止当前出错的这条语句,后续的语句照样继续执行。这类错误也能被try…catch捕获到。
为了保证整个事务中,任何语句出现错误都回滚整个事务,最简单的方法是在事务开始前设置SET XACT_ABORT 为 ON,这个设置指示数据引擎,在一个事务中遇到一个错误后,不再执行后续的事务,并回滚整个事务。
l 编译错误
遇到编译错误时,错误语句所在的批次不被执行,并不会受SET XACT_ABORT设置的影响。
1.2.自动提交事务(Autocommit Transactions)
这个模式是数据引擎的缺省模式,也是各种编程接口的事务缺省模式。每个单独的语句在完成后被提交,失败后被回滚,编程人员不需要指定任何命令。
每个单独的语句就是一个事务的单位,成功了就提交,这句语句执行错误就回滚这条语句,对其他语句的执行不产生影响。注意这里说的执行错误是运行时错误,如果语句本身有编译错误,比如sql语句的关键词拼写错误了,那么发生编译错误语句所在的那个批次的语句都将不被执行。比如:
USE AdventureWorks; GO CREATE TABLE TestBatch (Cola INT PRIMARY KEY, Colb CHAR(3)); GO INSERT INTO TestBatch VALUES (1, 'aaa'); INSERT INTO TestBatch VALUES (2, 'bbb'); INSERT INTO TestBatch VALUSE (3, 'ccc'); -- Syntax error. GO SELECT * FROM TestBatch; -- Returns no rows. GO |
上面这段sql中的第三个insert语句values关键字拼写错误,将导致编译错误,结果是跟这个语句在同一批次的所有三条insert语句都将不被执行。
如果上面第三个insert语句是这样的:
INSERT INTO TestBatch VALUES (1, 'ccc'); -- Duplicate key error.
这将产生一个运行时错误“重复的主键”,这条语句将被回滚,但是不影响前面两条insert语句。从这点可以看出,自动提交模式是每条单独的语句要么完成要么回滚,不影响其他语句的执行。
1.3.隐式事务(Implicit Transactions)
在SET IMPLICIT_TRANSACTIONS ON命令之后的第一条语句开始,就开始一个新的事务,直到遇到COMMIT 或 ROLLBACK语句结束这个事务,下一个语句又是一个新的事务,同样直到遇到COMMIT 或 ROLLBACK语句结束这个事务。这样形成了一个事务链,直到SET IMPLICIT_TRANSACTIONS OFF结束隐式事务,回到默认的自动提交事务模式。
事务中的行为跟显式事务模式是一致的。
事务体现在connection的水平,一个connection具有事务模式,自动提交模式是connection的缺省事务模式,直到BEGIN TRANSACTION语句开始显式事务模式,或者隐式事务被SET IMPLICIT_TRANSACTIONS ON设置,连接的事务模式被置为显式或隐式事务模式,当显示事务被提交或者回滚,隐式事务被置为关闭后,这个连接的事务模式又被置为自动提交模式。
2. 事务的编程
数据库的编程有两种方式,一种应用程序接口(API),包括ODBC、ADO 、ado.net等等编程接口,一种是Transact-SQL脚本,典型的是存储过程。
2.1.Transact-SQL脚本
BEGIN TRANSACTION
标记显式连接事务的起始点。
COMMIT TRANSACTION 或 COMMIT WORK
如果没有遇到错误,可使用该语句成功地结束事务。该事务中的所有数据修改在数据库中都将永久有效。事务占用的资源将被释放。
ROLLBACK TRANSACTION 或 ROLLBACK WORK
用来回滚遇到错误的事务。该事务修改的所有数据都返回到事务开始时的状态。事务占用的资源将被释放。
2.2.ADO.NET应用程序接口
对 SqlConnection 对象使用 BeginTransaction 方法可以启动一个显式事务。若要结束事务,可以对 SqlTransaction 对象调用 Commit() 或 Rollback() 方法。
下面主要以在存储过程中使用事务的编程详加说明
使用事务的目的是保持一段sql语句执行的完整性,要么全部执行成功,只要有一条语句失败就能完全回滚,回到事务开始前的状态。
事务有起点,即通过BEGIN TRANSACTION启动一个事务,其后执行事务中的各个语句,最后要判断,全部语句执行都成功了,就用COMMIT TRANSACTION提交事务,把事务中执行的语句的结果固定下来;如果事务中有任何错误,要能捕获到错误,并执行ROLLBACK TRANSACTION回滚整个事务。
下面是一段示例代码:
USE AdventureWorks; BEGIN TRANSACTION; BEGIN TRY -- 产生一个违反约束的错误. DELETE FROM Production.Product WHERE ProductID = 980; END TRY BEGIN CATCH SELECT ERROR_NUMBER() AS ErrorNumber, ERROR_SEVERITY() AS ErrorSeverity, ERROR_STATE() as ErrorState, ERROR_PROCEDURE() as ErrorProcedure, ERROR_LINE() as ErrorLine, ERROR_MESSAGE() as ErrorMessage; IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION; END CATCH; IF @@TRANCOUNT > 0 COMMIT TRANSACTION; |
把事务中要执行的语句都放在TRY语句块中,保证所有语句产生错误都能被捕获到。如果事务中的语句一旦产生错误,事务中的后续语句不再被执行,直接跳到CATCH语句块执行,进行出错后的后续处理过程。
CATCH语句块中的最主要的工作就是执行事务回滚语句,以回滚整个事务。也可以进行一些其他辅助性的工作,显示错误,记录错误等等。
如果事务中所有语句都没有出错,顺利执行完成,程序就跳过CATCH语句块,执行最后的COMMIT TRANSACTION提交事务。
经常看到有些人使用@@error来捕获错误,判断是否需要回滚事务,代码大概如下:
BEGIN TRANSACTION; Select xxx from yyyy; --事务中的sql语句 …… If @@error > 0 ROLLBACK TRANSACTION; Else COMMIT TRANSACTION; |
这里使用@@error来判断事务中所有的语句是否发生错误,并以此来决定是回滚事务,还是提交事务。实际上这么做是是十分错误的。
第一,@@error是针对每个sql语句执行结果,反映的是当前执行的语句出错状态,当执行到下一句,@@error又被重置以反应下一句语句的执行结果。所以用@@error来判断所有语句是否出错是不行的。
第二,sql语句的运行时错误有两类,一类是语句发生了错误,此语句被中止,但后续语句还能被继续执行,一类是语句发生错误后,一个命令批次中的后续的所有语句也不再被执行。当事务中的语句发生这种错误,那么放在最后的If @@error > 0判断语句都不会有机会被执行了。
这样的做法可能导致很严重的后果:如果事务中有语句产生第一类的错误,后续语句都不被执行,原来设计的ROLLBACK TRANSACTION或COMMIT TRANSACTION都没有机会被执行,就是说被这个事务锁了的资源都将得不到释放,产生的后果是,如果这个事务对某些记录设置了共享锁,那这些记录再也不能被修改,更惨的是如果这个事务对某些记录设置了排他锁,那么读取这些记录的语句一直会被堵塞,执行不下去了。程序也就死在那里了。
所以,在事务中用来捕获语句错误还是需要使用try…catch语句块。