从一个存储过程启动3个存储过程时如何回滚


23

我有一个存储过程,其中仅执行3个存储过程。如果主控SP成功,我仅使用1个参数进行存储。

如果第一个存储过程在主存储过程中工作正常,但是第二个存储过程失败,那么它将自动回滚主SP中的所有SP还是我必须发出一些命令?

这是我的程序:

CREATE PROCEDURE [dbo].[spSavesomename] 
    -- Add the parameters for the stored procedure here

    @successful bit = null output
AS
BEGIN
begin transaction createSavebillinginvoice
    begin Try
    -- SET NOCOUNT ON added to prevent extra result sets from
    -- interfering with SELECT statements.
    SET NOCOUNT ON;

   BEGIN 

   EXEC [dbo].[spNewBilling1]

   END

   BEGIN 

   EXEC [dbo].[spNewBilling2]

   END

   BEGIN 

   EXEC [dbo].[spNewBilling3]

   END 

   set @successful  = 1

   end Try

    begin Catch
        rollback transaction createSavesomename
        insert into dbo.tblErrorMessage(spName, errorMessage, systemDate) 
             values ('spSavesomename', ERROR_MESSAGE(), getdate())

        return
    end Catch
commit transaction createSavesomename
return
END

GO

如果spNewBilling3抛出错误,但是您不想回滚spNewBilling2spNewBilling1,则只需[begin|rollback|commit] transaction createSavebillinginvoice从中删除spSavesomename
迈克

Answers:


56

仅给出问题中显示的代码,并假定三个子过程均未进行任何显式的事务处理,那么是的,将捕获三个子过程中的任何一个错误,并且ROLLBACKCATCH块中的in 将回滚所有工作。

但是以下是有关事务的一些注意事项(至少在SQL Server中):

  • 无论您叫多少次,只有一次真实交易(第一个)BEGIN TRAN

    • 您可以命名一个事务(如你在这里做)这个名字将出现在日志中,但命名仅对于第一/最外层的交易(因为再次,第一个是交易)。
    • 每次调用时BEGIN TRAN,无论它是否命名,事务计数器都会增加1。
    • 您可以通过执行以下操作查看当前级别 SELECT @@TRANCOUNT;
    • 在2或更高COMMIT时发出的任何命令@@TRANCOUNT无非是一次减少一个事务计数器。
    • 在at COMMIT发出a之前,不会提交任何内容@@TRANCOUNT1
    • 万一上面的信息不能清楚地表明:无论交易级别如何,都没有实际的交易嵌套。
  • 保存点允许事务中创建可以撤消的工作子集。

    • 通过SAVE TRAN {save_point_name}命令创建/标记保存点
    • 保存点标记了可以撤消而无需回滚整个事务的工作子集的开始
    • 保存点名称不必唯一,但是多次使用相同的名称仍会创建不同的保存点。
    • 保存点可以嵌套。
    • 保存点无法提交。
    • 可以通过撤消保存点ROLLBACK {save_point_name}。(有关此内容,请参见下文)
    • 回滚保存点将撤消最近一次对的调用之后发生的任何工作SAVE TRAN {save_point_name},包括在创建被回滚的保存点之后创建的所有保存点(因此称为“嵌套”)。
    • 回滚保存点不会影响事务计数/级别
    • SAVE TRAN除非发出完整ROLLBACK的整个交易记录,否则无法撤消在初始记录之前所做的任何工作。
    • 只是要清楚一点:发出大于等于2 的COMMITwhen @@TRANCOUNT不会影响保存点(因为再次,大于1的交易级别在该计数器之外不存在)。
  • 您不能提交特定的命名交易。事务“名称”(如果与一起提供)将COMMIT被忽略,并且仅出于可读性而存在。

  • ROLLBACK没有名称的发行将始终回滚所有交易。

  • ROLLBACK具有名称的签发人必须符合以下任一条件:

    • 假设第一个事务被命名为:
      假设没有SAVE TRAN使用相同的事务名称调用no ,则将回滚所有事务。
    • “保存点”(如上所述):
      此行为将“撤消”自最近 SAVE TRAN {save_point_name}一次调用以来所做的所有更改。
    • 如果第一个事务是a)命名的,并且b)SAVE TRAN发出了带有其名称的命令,则该事务名称的每个ROLLBACK将撤消每个保存点,直到该名称没有剩余为止。之后,发出该名称的ROLLBACK将回滚所有事务。
    • 例如,假定以下命令以显示的顺序运行:

      BEGIN TRAN A -- @@TRANCOUNT is now 1
      -- DML Query 1
      SAVE TRAN A
      -- DML Query 2
      SAVE TRAN A
      -- DML Query 3
      
      BEGIN TRAN B -- @@TRANCOUNT is now 2
      SAVE TRAN B
      -- DML Query 4

      现在,如果您发出(以下每种情况彼此独立):

      • ROLLBACK TRAN B一次:它将撤消“ DML查询4”。@@TRANCOUNT仍然是2。
      • ROLLBACK TRAN B两次:它将撤消“ DML查询4”,然后由于没有对应的“ B”保存点而出错。@@TRANCOUNT仍然是2。
      • ROLLBACK TRAN A一次:它将撤消“ DML查询4”和“ DML查询3”。@@TRANCOUNT仍然是2。
      • ROLLBACK TRAN A两次:它将撤消“ DML查询4”,“ DML查询3”和“ DML查询2”。@@TRANCOUNT仍然是2。
      • ROLLBACK TRAN A三次:撤消“ DML查询4”,“ DML查询3”和“ DML查询2”。然后它将回滚整个事务(剩下的只是“ DML查询1”)。@@TRANCOUNT现在是0。
      • COMMIT一次:@@TRANCOUNT下降到1。
      • COMMIT一次ROLLBACK TRAN B又一次:@@TRANCOUNT下降到1。然后它将撤消“ DML查询4”(证明COMMIT没有执行任何操作)。@@TRANCOUNT仍然是1。
  • 交易名称和保存点名称:

    • 最多可以包含32个字符
    • 不管实例级或数据库级排序规则如何,都将被视为具有二进制排序规则(如当前文档所述,不区分大小写)。
    • 有关详细信息,请参见以下文章的“ 事务名称”部分:名称中有什么?:在T-SQL标识符的古怪世界中
  • 存储过程本身不是隐式事务。如果没有显式事务已启动,则每个查询都是一个隐式事务。这就是为什么除非有程序上的原因要执行围绕单个查询的显式事务ROLLBACK,否则查询中的任何错误都是该查询的自动回滚。

  • 调用存储过程时,它必须以与@@TRANCOUNT调用时相同的值退出。意思是,您不能:

    • BEGIN TRAN在不提交的情况下在proc中启动a ,希望在调用/父进程中进行提交。
    • ROLLBACK如果显式事务在调用proc之前已启动,则无法发出a ,因为它将返回@@TRANCOUNT0。

    如果退出的存储过程的事务计数大于或小于它被启动时的事务计数,则会出现类似于以下的错误:

    消息266,级别16,状态2,过程YourProcName,第0行
    EXECUTE之后的事务计数指示BEGIN和COMMIT语句的数量不匹配。上一个计数= X,当前计数=Y。

  • 与常规变量一样,表变量不受事务约束。


关于可以独立调用(因此需要事务处理)或从其他proc调用(因此不需要事务处理)的proc中的事务处理:这可以通过几种不同的方式来实现。

我一直在处理,现在它好几年,似乎工作很好的方式是只BEGIN/ COMMIT/ ROLLBACK在最外层。子过程调用只是跳过事务命令。我在下面概述了我在每个proc中放置的内容(嗯,每个需要事务处理的东西)。

  • 在每个过程的顶部, DECLARE @InNestedTransaction BIT;
  • 代替simple BEGIN TRAN,请执行以下操作:

    IF (@@TRANCOUNT = 0)
    BEGIN
       SET @InNestedTransaction = 0;
       BEGIN TRAN; -- only start a transaction if not already in one
    END;
    ELSE
    BEGIN
       SET @InNestedTransaction = 1;
    END;
  • 代替simple COMMIT,请执行以下操作:

    IF (@@TRANCOUNT > 0 AND @InNestedTransaction = 0)
    BEGIN
       COMMIT;
    END;
  • 代替simple ROLLBACK,请执行以下操作:

    IF (@@TRANCOUNT > 0 AND @InNestedTransaction = 0)
    BEGIN
       ROLLBACK;
    END;

无论事务是在SQL Server中启动还是在应用程序层启动,此方法都应相同。

有关该事务处理中的完整模板的TRY...CATCH构造,请参见以下DBA.SE问题的答案:我们是否需要使用C#代码以及存储过程来处理事务


除了“基础”以外,还有一些其他细微的事务需要注意:

  • 默认情况下,在大多数情况下,发生错误时,事务不会自动回滚/取消。只要您有适当的错误处理并ROLLBACK自我打扰,通常这不是问题。但是,有时情况会变得复杂,例如在中止批处理错误的情况下,或者在使用时OPENQUERY(或通常使用链接服务器),并且在远程系统上会发生错误。虽然大多数错误都可以使用来捕获TRY...CATCH,但是有两个无法以这种方式捕获(尽管现在还不记得正在搜索哪些错误—研究)。在这些情况下,必须使用SET XACT_ABORT ON来正确回滚事务。

    SET XACT_ABORT ON会使SQL Server 立即回滚任何事务(如果一个事务处于活动状态),在发生任何错误时中止批处理。此设置在引入该TRY...CATCH构造的SQL Server 2005之前存在。在TRY...CATCH大多数情况下,它可以处理大多数情况,因此在大多数情况下会使对的需求过时XACT_ABORT ON。但是,在使用时OPENQUERY(可能还有我目前不记得的另一种情况),您仍然需要使用SET XACT_ABORT ON;

  • 在触发器内部,XACT_ABORT被隐式设置为ON。这将导致触发器内的任何错误,从而取消触发触发器的整个DML语句。

  • 您应该始终具有正确的错误处理,尤其是在使用事务时。TRY...CATCHSQL Server 2005中引入的构造提供了一种处理几乎所有情况的方法,这是对@@ERROR每个语句之后进行测试的可喜的改进,这对于中止批处理错误没有太大帮助。

    TRY...CATCH但是,引入了一个新的“状态”。当使用该TRY...CATCH构造时,如果您有活动的事务并且发生错误,那么可以采用以下几种路径:

    • XACT_ABORT OFF和语句中止错误:事务仍处于活动状态,并且继续处理下一条语句(如果有)。
    • XACT_ABORT OFF以及大多数中止批次的错误:事务仍处于活动状态,并且继续处理下一个批次(如果有)。
    • XACT_ABORT OFF以及某些中止批次的错误:事务已回滚,并且下一个批次(如果有)继续进行处理。
    • XACT_ABORT ON并发生任何错误:事务已回滚,并且处理将继续进行下一批(如果有)。


    但是,使用时TRY...CATCH,批中止错误不会中止批处理,而是将控制权转移到CATCH块。如果XACT_ABORTOFF,则交易将在大多数时间仍处于活动状态,您将需要COMMIT或很有可能需要ROLLBACK。但是,当遇到某些中止批次的错误(例如使用OPENQUERY)时,或者当XACT_ABORTis时ON,事务将处于新状态“不可提交”。在这种状态下,您将不能COMMIT执行DML操作。您所能做的就是ROLLBACKSELECT声明。但是,在这种“无法使用”状态下,事务在发生错误时已回滚,并且发出ROLLBACK仅仅是一种形式,但这是必须完成的一种形式。

    函数XACT_STATE可以用于确定事务是处于活动状态,不可提交状态还是不存在。建议(至少有人)在程序段中检查此功能CATCH以确定结果是否为-1(即不可提交),而不是测试if @@TRANCOUNT > 0。但是,使用XACT_ABORT ON,这应该是唯一可能的状态,因此似乎测试@@TRANCOUNT > 0XACT_STATE() <> 0是等效的。另一方面,当XACT_ABORTOFF且有一个活动的交易时,则有可能处于1-1处于CATCH块状的状态,这使得有可能发行COMMIT而不是ROLLBACK(尽管我无法想到某人想要COMMIT如果交易是可提交的)。我对以下DBA.SE问题的回答中提供了有关XACT_STATE()CATCH块内使用的更多信息和研究XACT_ABORT ON在什么情况下,当XACT_ABORT设置为ON时,可以从CATCH块内部提交事务?。请注意,在某些情况下,有一个小错误XACT_STATE()会导致它错误地返回1XACT_STATE()在带有某些系统变量但没有FROM子句的SELECT中使用时返回1。


关于原始代码的注释:

  • 您可以删除为交易指定的名称,因为它无济于事。
  • 您不需要每次通话都使用BEGINENDEXEC

2
这是一个很好的答案。
McNets '17

1
哇,这是一个全面的答案!谢谢!顺便说一句,下一页是否解决了您所暗示的Try ... Catch没有捕获的错误?(在“未受TRY…CATCH构造影响的错误标题下?” technet.microsoft.com/zh-cn/library/ms175976(v=sql.110).aspx
jrdevdba

1
@jrdevdba谢谢:-)。欢迎您。关于未捕获的错误,我几乎指的是以下两个:Compile errors, such as syntax errors, that prevent a batch from runningErrors that occur during statement-level recompilation, such as object name resolution errors that occur after compilation because of deferred name resolution.。但是它们并不经常发生,当您发现这种情况时,要么对其进行修复(如果这是代码中的错误),要么将其置于子进程(EXECsp_executesql)中,以便TRY...CATCH可以捕获它。
所罗门·鲁兹基

2

是的,如果由于任何错误而将执行主存储过程的catch语句中的回滚代码,它将回滚由任何直接语句或其中的任何嵌套存储过程执行的所有操作。

即使您没有在嵌套存储过程中应用任何显式事务,这些存储过程仍将使用隐式事务,并且将在完成时提交,但无论您是通过嵌套存储过程中的显式还是隐式事务提交的,SQL Server引擎都将忽略它,并且会如果主存储过程失败并且事务已回滚,则回滚这些嵌套存储过程的所有操作。

每次根据最外部事务结束时所采取的操作来落实或回退事务。如果外部事务已提交,则内部嵌套事务也将提交。如果外部事务回滚,那么所有内部事务也会回滚,而不管内部事务是否单独提交。

供参考http://technet.microsoft.com/zh-cn/library/ms189336(v=sql.105).aspx

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.