问题 在SQL Server 2014中使用TransactionScope围绕存储过程与事务


我正在使用C#和ADO.Net TransactionScope 在ASP.Net应用程序中运行事务。此事务应该在多个表中保存一些数据,然后向订阅者发送电子邮件。

:它是否有效使用 TransactionScope,当它包含对SQL Server 2014中具有自己的事务的存储过程的调用时,或者我应该删除SQL事务语句,即 begin trancommit tran 和 rollback tran 来自存储过程的语句在此中被调用 TransactionScope

下面提到了此场景的C#代码以及存储过程的T-SQL代码。

使用C#代码 TransactionScope

  try 
    {
        using (TransactionScope scope = new TransactionScope())
        {
            using (SqlConnection connection1 = new SqlConnection(connectString1))
            {
                // Opening the connection automatically enlists it in the  
                // TransactionScope as a lightweight transaction.
                connection1.Open();

                // SaveEmailData is a stored procedure that has a transaction within it
                SqlCommand command1 = new SqlCommand("SaveEmailData", connection1);
                command1.CommandType = CommandType.StoredProcedure;
                command1.ExecuteNonQuery();

            }

            //Send Email using the helper method
            EmailHelper.SendCustomerEmails(customerIds);

            // The Complete method commits the transaction. If an exception has been thrown, 
            // Complete is not  called and the transaction is rolled back.
            scope.Complete();

        }
    }
    catch( Exception ex)
    {
       Logger.Log(ex);
    }

存储过程的T-SQL SaveEmailData

SET NOCOUNT ON

    BEGIN TRY
        DECLARE @emailToUserId BIGINT

        BEGIN TRAN
        -- //update statement. detail statement omitted
        UPDATE TABLE1...

         --update statement. detail statement omitted
        UPDATE TABLE2...

        IF @@trancount > 0
        BEGIN
            COMMIT TRAN
        END
    END TRY

    BEGIN CATCH

        IF @@TRANCOUNT > 0
        BEGIN
            ROLLBACK TRAN
        END

        EXEC Error_RaiseToADONET

    END CATCH

11254
2017-07-15 04:53


起源



答案:


是, TransactionScope 包装TSQL时仍然可以工作 BEGIN / COMMIT TRANSACTION 或ADO SqlConnection.BeginTransaction。包装单个连接时,行为类似于嵌套事务 Sql

  • @@TranCount 将在每个上增加 BEGIN TRAN

  • COMMIT TRAN 将简单地减少 @@TRANCOUNT。只有在以下情况下才会提交交易 @@TRANCOUNT 命中零。

然而:

  • ROLLBACK TRAN 将中止整个交易(即 @@ TRANCOUNT为零),除非你使用 保存积分 (即 SAVE TRANSACTION xx ... ROLLBACK TRANSACTION xx
  • 使用存储过程时,如果连接,您将收到错误 @@TRANCOUNT 退出SPROC时与进入SPROC时的价值不同。

因此,将事务语义留下来通常要容易得多 TransactionScope 并删除任何手册 BEGIN TRAN / COMMIT TRAN 混乱你的TSQL的逻辑。

编辑  - 澄清以下评论

  • 在OP的情况下,SPROC并未编写嵌套事务(即,是否由Sql或.Net外部事务包装),具体而言, ROLLBACK 在里面 BEGIN CATCH block将中止整个外部事务,并可能导致外部进一步的错误 TransactionScope 作为 @@TRANCOUNT 规则没有得到遵守。一个 嵌套的事务模式,例如this 如果SPROC需要以嵌套或独立的事务方式运行,应该遵守。

  • SavePoints不适用于分布式事务,和 TransactionScope 很容易 升级为分布式事务 例如如果您在事务范围内使用不同的连接字符串或控制其他资源。

因此,我建议将PROC重构为一个“快乐”的核心/内部案例,从事务范围调用此内部proc,并在那里进行任何异常处理和回滚。如果你还需要从Ad Hoc Sql调用proc,那么提供一个外部包装器Proc,它具有异常处理:

-- Just the happy case. This is called from .Net TransactionScope
CREATE PROC dbo.InnerNonTransactional
  AS
    BEGIN 
      UPDATE TABLE1...
      UPDATE TABLE2 ....
    END;

-- Only needed if you also need to call this elsewhere, e.g. from AdHoc Sql
CREATE PROC dbo.OuterTransactional
  AS
    BEGIN
      BEGIN TRY
        BEGIN TRAN
            EXEC dbo.InnerNonTransactional
        COMMIT TRAN
      END TRY
      BEGIN CATCH
         -- Rollback and handling code here.
      END CATCH
    END;

13
2017-07-15 05:02



谢谢你的详细解答。你的意思是如果在存储过程中调用'rollback tran',那么即使没有从程序中引发错误,TransactionScope也会自动回滚整个事务而不仅仅是存储过程事务? - Sunil
是的,打电话 ROLLBACK TRAN 从TSQL开始,连接将取消在Conn上完成的任何其他工作,例如由ADO。只有你使用 SAVEPOINT 你能限制ROLLBACK的范围吗?但请注意 保存点 不使用分布式事务,这可能与TransactionScope非常微妙地发生,例如,如果同时在范围中打开多个conn。我找到了保留的唯一理由 BEGIN/COMMIT TRAN 在.Net App调用的SPROC中,如果SPROC也需要在其他地方执行,例如来自SSMS的ad-hoc。 - StuartLC
但是,存储过程中的rollback tran不会回滚TransactionScope事务,除非在调用rollback tran语句时也会从存储过程中引发错误? - Sunil
不,ROLLBACK将杀死 outermost 交易。我相信你在找 保存点数 用标准模式 这里实施。正如您所看到的,逻辑更复杂,因此建议避免使用保存点进行部分回滚,而是控制事务 .Net 它也可以使用 SavePoints通过 SqlClient - StuartLC
好。我知道了。我猜TransactionScope对象正在跟踪数据库连接是否已回滚事务,因此您所说的事情发生了。 - Sunil


答案:


是, TransactionScope 包装TSQL时仍然可以工作 BEGIN / COMMIT TRANSACTION 或ADO SqlConnection.BeginTransaction。包装单个连接时,行为类似于嵌套事务 Sql

  • @@TranCount 将在每个上增加 BEGIN TRAN

  • COMMIT TRAN 将简单地减少 @@TRANCOUNT。只有在以下情况下才会提交交易 @@TRANCOUNT 命中零。

然而:

  • ROLLBACK TRAN 将中止整个交易(即 @@ TRANCOUNT为零),除非你使用 保存积分 (即 SAVE TRANSACTION xx ... ROLLBACK TRANSACTION xx
  • 使用存储过程时,如果连接,您将收到错误 @@TRANCOUNT 退出SPROC时与进入SPROC时的价值不同。

因此,将事务语义留下来通常要容易得多 TransactionScope 并删除任何手册 BEGIN TRAN / COMMIT TRAN 混乱你的TSQL的逻辑。

编辑  - 澄清以下评论

  • 在OP的情况下,SPROC并未编写嵌套事务(即,是否由Sql或.Net外部事务包装),具体而言, ROLLBACK 在里面 BEGIN CATCH block将中止整个外部事务,并可能导致外部进一步的错误 TransactionScope 作为 @@TRANCOUNT 规则没有得到遵守。一个 嵌套的事务模式,例如this 如果SPROC需要以嵌套或独立的事务方式运行,应该遵守。

  • SavePoints不适用于分布式事务,和 TransactionScope 很容易 升级为分布式事务 例如如果您在事务范围内使用不同的连接字符串或控制其他资源。

因此,我建议将PROC重构为一个“快乐”的核心/内部案例,从事务范围调用此内部proc,并在那里进行任何异常处理和回滚。如果你还需要从Ad Hoc Sql调用proc,那么提供一个外部包装器Proc,它具有异常处理:

-- Just the happy case. This is called from .Net TransactionScope
CREATE PROC dbo.InnerNonTransactional
  AS
    BEGIN 
      UPDATE TABLE1...
      UPDATE TABLE2 ....
    END;

-- Only needed if you also need to call this elsewhere, e.g. from AdHoc Sql
CREATE PROC dbo.OuterTransactional
  AS
    BEGIN
      BEGIN TRY
        BEGIN TRAN
            EXEC dbo.InnerNonTransactional
        COMMIT TRAN
      END TRY
      BEGIN CATCH
         -- Rollback and handling code here.
      END CATCH
    END;

13
2017-07-15 05:02



谢谢你的详细解答。你的意思是如果在存储过程中调用'rollback tran',那么即使没有从程序中引发错误,TransactionScope也会自动回滚整个事务而不仅仅是存储过程事务? - Sunil
是的,打电话 ROLLBACK TRAN 从TSQL开始,连接将取消在Conn上完成的任何其他工作,例如由ADO。只有你使用 SAVEPOINT 你能限制ROLLBACK的范围吗?但请注意 保存点 不使用分布式事务,这可能与TransactionScope非常微妙地发生,例如,如果同时在范围中打开多个conn。我找到了保留的唯一理由 BEGIN/COMMIT TRAN 在.Net App调用的SPROC中,如果SPROC也需要在其他地方执行,例如来自SSMS的ad-hoc。 - StuartLC
但是,存储过程中的rollback tran不会回滚TransactionScope事务,除非在调用rollback tran语句时也会从存储过程中引发错误? - Sunil
不,ROLLBACK将杀死 outermost 交易。我相信你在找 保存点数 用标准模式 这里实施。正如您所看到的,逻辑更复杂,因此建议避免使用保存点进行部分回滚,而是控制事务 .Net 它也可以使用 SavePoints通过 SqlClient - StuartLC
好。我知道了。我猜TransactionScope对象正在跟踪数据库连接是否已回滚事务,因此您所说的事情发生了。 - Sunil