sql 嵌套事务学习笔记
以下内容根据此官方文档修改:http://technet.microsoft.com/zh-cn/library/ms189336(v=sql.105).aspx
嵌套事务的使用场景或者说目的主要是为了调用包含了事务的存储过程。不然没必要使用嵌套事务。
下列示例显示了嵌套事务的用途。在TransProc 存储过程中包含事务,在另外的代码中分别启动事务调用TransProc和不启动事务调用TransProc。
SET QUOTED_IDENTIFIER OFF; GO SET NOCOUNT OFF; GO USE AdventureWorks2008R2; GO CREATE TABLE TestTrans(Cola INT PRIMARY KEY, Colb CHAR(3) NOT NULL); GO CREATE PROCEDURE TransProc @PriKey INT, @CharCol CHAR(3) AS BEGIN TRANSACTION InProc INSERT INTO TestTrans VALUES (@PriKey, @CharCol) INSERT INTO TestTrans VALUES (@PriKey + 1, @CharCol) COMMIT TRANSACTION InProc; GO /* Start a transaction and execute TransProc. */ BEGIN TRANSACTION OutOfProc; GO EXEC TransProc 1, 'aaa'; GO /* Roll back the outer transaction, this will roll back TransProc's nested transaction. */ ROLLBACK TRANSACTION OutOfProc; GO EXECUTE TransProc 3,'bbb'; GO /* The following SELECT statement shows only rows 3 and 4 are still in the table. This indicates that the commit of the inner transaction from the first EXECUTE statement of TransProc was overridden by the subsequent rollback. */ SELECT * FROM TestTrans; GO
嵌套事务有以下特点:
1、SQL Server 数据库引擎将忽略内部事务的提交,除了将@@TRANCOUNT 减 1。内部事务的真正提交或者回滚是依靠最外部事务结束时进行的提交或者回滚。如果提交外部事务,也将提交内部嵌套事务。如果回滚外部事务,也将回滚所有内部事务,不管是否单独提交过内部事务。
2、但针对第1条,假如内部事务进行了提交动作(COMMIT TRANSACTION 或 COMMIT WORK),COMMIT只对当前所处的TRANSACTION起作用,也就是说即使COMMIT TRANSACTION transaction_name中的transaction_name是外部事务的名称,也不会提交该外部事务。这一条的重点在于内部事务无法提交外部事务。
3、@@TRANCOUNT 函数记录当前事务的嵌套级别,@@TRANCOUNT=0表示不在事务中,等于1表示在一个事务中,大于1表示处在嵌套事务中。每次 BEGIN TRANSACTION 语句使 @@TRANCOUNT 增加 1,每次 COMMIT TRANSACTION 或 COMMIT WORK 语句使 @@TRANCOUNT 减去 1,而只要有一个ROLLBACK就会使@@TRANCOUNT等于0.
4、ROLLBACK TRANSACTION 语句的 transaction_name只能是最外部事务的事务名称,使用内部事务名称是非法的。带最外部事务名称的ROLLBACK或者不带任何名称的ROLLBACK语句都将回滚所有嵌套事务,包括最外部事务。此时@@TRANCOUNT等于0。
5、针对第4条,这里特别需要说明的是:虽然这里说“ROLLBACK语句都将回滚所有嵌套事务,包括最外部事务”,但这里的前提是在最外层进行ROLLBACK,经过本人亲自实验,如果在内部事务中执行带最外部事务名称的ROLLBACK或者不带任何名称的ROLLBACK,则只回滚当前内部事务和已执行过的外部事务语句,此内部事务后续的外层事务将继续执行,并能成功修改数据,但后续外部事务中的所有rollback和commit都将不起作用,并提示错误信息,因为@@TRANCOUNT早已经等于0,数据库引擎找不到对应的BEGIN TRANSACTION。可以自己通过下面代码示例中的bad code示例进行验证。
以下最佳代码示范说明参考以下博文:http://www.cnblogs.com/Kymo/archive/2008/05/14/1194161.html
下面用代码进行解释,代码是根据Online Help Commit Transaction一节的代码修改而成,首先建立一个Table,然后开始三个Trasaction,中间人为触发一些错误,然后观察运行结果。
--Bad code USE NORTHWIND; --Create test table IF Object_id(N'TestTran',N'U') IS NOT NULL DROP TABLE TESTTRAN; CREATE TABLE TESTTRAN ( COLA INT PRIMARY KEY, COLB CHAR(3)); --Variable for keeping @@ERROR DECLARE @_Error INT; SET @_Error = 0; --Begin 3 nested transaction BEGIN TRANSACTION OUTERTRAN; BEGIN TRANSACTION INNER1; BEGIN TRANSACTION INNER2; INSERT INTO TESTTRAN VALUES (3,'ccc');--Inner2 RAISERROR('Inner2 error', 16, 1) IF @@ERROR = 0 COMMIT TRANSACTION INNER2; ELSE ROLLBACK TRANSACTION ; INSERT INTO TESTTRAN VALUES (2,'bbb');--Inner1 IF @@ERROR = 0 COMMIT TRANSACTION INNER1; ELSE ROLLBACK TRANSACTION ; INSERT INTO TESTTRAN VALUES (1,'aaa');--OuterTran --RAISERROR ('OuterTran error',16,1) IF @@ERROR = 0 COMMIT TRANSACTION OuterTran; ELSE ROLLBACK TRANSACTION; SELECT * FROM TESTTRAN (NOLOCK); SELECT @@Trancount;
上述代码当内层事务发生错误时,并不能正常Rollback,因为Rollback把@@Trancount变成了0,所以后面的Commit语句就找不到对应的Transaction了。解决问题的关键就是Rollback时要判断@@Trancount,当@@Trancount等于1时进行Rollback进行回滚,否则执行Commit把@@Trancount-1,同时把@@Error传到外层事务交给外层事务处理。微软的原文是没有问题的,但是这种情况比较简单,我们一眼就能看出哪个是内层事务,哪个是外层事务,一共嵌套了几层,如果是SP调用呢?你不知道你的SP会被谁调用,也不知道会被嵌套几层。
下面看一下怎么处理内层事务的错误(何时Rollback, Commit及错误的传递)
--Good code USE NORTHWIND; --Create test table IF Object_id(N'TestTran',N'U') IS NOT NULL DROP TABLE TESTTRAN; CREATE TABLE TESTTRAN ( COLA INT PRIMARY KEY, COLB CHAR(3)); --Variable for keeping @@ERROR DECLARE @_Error INT; SET @_Error = 0; --Begin 3 nested transaction BEGIN TRANSACTION OUTERTRAN; BEGIN TRANSACTION INNER1; BEGIN TRANSACTION INNER2; INSERT INTO TESTTRAN VALUES (3,'ccc');--Inner2 --raiserror('Inner2 error', 16, 1) SET @_Error = @@ERRORIF @_Error = 0 COMMIT TRAN INNER2; ELSE IF @@TRANCOUNT > 1 COMMIT TRANSACTION INNER2; ELSE ROLLBACK TRANSACTION INNER2; INSERT INTO TESTTRAN VALUES (2,'bbb');--Inner1 IF @_Error = 0 SET @_Error = @@ERRORIF @_Error = 0 COMMIT TRAN INNER1; ELSE IF @@TRANCOUNT > 1 COMMIT TRANSACTION INNER1; ELSE ROLLBACK TRANSACTION INNER1; INSERT INTO TESTTRAN VALUES (1,'aaa');--OuterTran RAISERROR ('OuterTran error',16,1) -- rollback transaction OuterTran SET @_Error = @_Error + @@ERROR IF @_Error = 0 COMMIT TRAN OUTERTRAN; ELSE IF @@TRANCOUNT > 1 COMMIT TRANSACTION; ELSE ROLLBACK TRANSACTION OUTERTRAN; SELECT * FROM TESTTRAN (NOLOCK)
考虑到SP的调用,我们开发SP时应该在最后把@@ERROR返回供调用者检查。另外测试注意检查一下@@Trancount,有时结果看似正确,但是如果@@Trancount不等于0,说明我们的代码出了问题。