在 SQL Server 2014 中围绕带有事务的存储过程使用 TransactionScope

Using TransactionScope around a stored procedure with transaction in SQL Server 2014

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

问题:当它包括对在 SQL 服务器中有自己的事务的存储过程的调用时,它是否是 TransactionScope 的有效使用2014,或者我应该删除 SQL 事务语句,即 begin trancommit tranrollback 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

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

中的嵌套事务
  • @@TranCount 将在每个 BEGIN TRAN

  • 上递增
  • COMMIT TRAN 只会递减 @@TRANCOUNT。仅当 @@TRANCOUNT 为零时才会提交事务。

但是:

  • ROLLBACK TRAN 将中止整个事务(即 @@TRANCOUNT to zero), unless you are using Save Points (即 SAVE TRANSACTION xx ... ROLLBACK TRANSACTION xx.
  • 使用存储过程时,如果退出 SPROC 时连接的 @@TRANCOUNT 与进入 SPROC 时的值不同,您将收到错误消息。

因此,通常更容易将事务语义留给 TransactionScope 并删除任何手动 BEGIN TRAN / COMMIT TRAN 逻辑以免使您的 TSQL 混乱。

编辑 - 澄清下面的评论

  • 在 OP 的情况下,在编写 SPROC 时并未考虑嵌套事务(即是否由 Sql 或 .Net 外部事务包装),具体来说,[=26 BEGIN CATCH 块中的 =] 将中止整个外部事务,并且可能会导致外部 TransactionScope 中出现更多错误,因为未遵守 @@TRANCOUNT 规则。如果 SPROC 需要以嵌套或独立事务方式运行,则应观察到 nested transaction pattern such as this

  • SavePoints do not work with Distributed transactions, and TransactionScope can easily escalate into a distributed transaction 例如如果您在事务范围内使用不同的连接字符串或控制其他资源。

因此,我建议将 PROC 重构为 'happy' 核心/内部情况,从事务范围调用此内部过程,并在那里进行任何异常处理和回滚。如果您还需要从 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;