检测查询是否在分布式事务中

Detect whether a query is in a distributed transaction

我需要一种可靠的方法来确定 SQL 服务器查询中的查询是否 运行 在分布式事务中。1 它分布式事务是在外部创建的还是使用 BEGIN DISTRIBUTED TRANSACTION 语句创建的并不重要 — 无论哪种方式,我都需要了解它。

我没有看到特定的 SQL Server function or stored procedure claiming to provide this information. There are some dynamic-management views whose documentation claims will provide this information, but the information is unreliable. For example, sys.dm_tran_session_transactions 有列 is_local:

1 = Local transaction.

0 = Distributed transaction or an enlisted bound session transaction.

所以,测试一下,利用SAVE TRANSACTIONis unsupported in a distributed transaction,会报错。2

此查询不在分布式事务中并按预期工作,为 is_local 选择值 1:

BEGIN TRANSACTION

SELECT s.is_local
FROM   sys.dm_tran_session_transactions s

SAVE TRANSACTION Error

ROLLBACK TRANSACTION

但是,如果我们用 BEGIN DISTRIBUTED TRANSACTION 替换 BEGIN TRANSACTIONis_local 仍然是 1,但是我们得到错误 "Cannot use SAVE TRANSACTION within a distributed transaction." 所以,我们不能依赖 is_local值。

sys.dm_tran_active_transactions怎么样?它的 transaction_type 列描述为:

Type of transaction.

1 = Read/write transaction

2 = Read-only transaction

3 = System transaction

4 = Distributed transaction

我们还需要一种方法来识别当前交易,sys.dm_tran_current_transaction 提供了这种方法。那么,让我们再测试一下:

BEGIN TRANSACTION

SELECT a.transaction_type
FROM   sys.dm_tran_current_transaction c
INNER JOIN sys.dm_tran_active_transactions a ON c.transaction_id = a.transaction_id

SAVE TRANSACTION Error

ROLLBACK TRANSACTION

对于此非分布式事务,我们得到值 1,但也可能是 2。但是,再次将 BEGIN TRANSACTION 替换为 BEGIN DISTRIBUTED TRANSACTION,我们会得到与 transaction_type 相同的值,但这次是 SAVE TRANSACTION 的错误。所以,我们也不能依赖transaction_type

为了确保问题不是分布式事务中的实际登记,我还尝试使用 C# 代码中的 TransactionScope

using System;
using System.Data;
using System.Data.SqlClient;
using System.Threading.Tasks;
using System.Transactions;
using IsolationLevel = System.Transactions.IsolationLevel;

namespace TransactionTroubleshooting
{
    class Program
    {
        private const string ConnectionString = "Server=.;Database=master;Trusted_Connection=True;";

        // Use C# 7.1 or later.
        public static async Task Main(string[] args)
        {
            try
            {
                await RunOuterTransaction();
            }
            catch (Exception e)
            {
                var current = e;
                while (current != null)
                {
                    Console.WriteLine(current.Message);
                    Console.WriteLine();
                    Console.WriteLine(current.StackTrace);
                    Console.WriteLine();
                    current = current.InnerException;
                }
            }
            finally
            {
                Console.WriteLine("Press a key...");
                Console.ReadKey();
            }   
        }

        private static async Task RunOuterTransaction()
        {
            using (var transaction = new TransactionScope(TransactionScopeOption.RequiresNew,
                new TransactionOptions {IsolationLevel = IsolationLevel.ReadCommitted},
                TransactionScopeAsyncFlowOption.Enabled))
            using (var connection = new SqlConnection(ConnectionString))
            {
                await connection.OpenAsync();
                using (var command = connection.CreateCommand())
                {
                    command.CommandType = CommandType.Text;
                    command.CommandText = @"
SELECT a.transaction_type
FROM   sys.dm_tran_current_transaction c
INNER JOIN sys.dm_tran_active_transactions a ON c.transaction_id = a.transaction_id
";
                    using (var reader = await command.ExecuteReaderAsync())
                    {
                        while (await reader.ReadAsync())
                        {
                            Console.WriteLine("Outer transaction_type is {0}", reader["transaction_type"]);
                        }
                    }
                }

                await RunInnerTransaction();
                transaction.Complete();
            }
        }

        private static async Task RunInnerTransaction()
        {
            // We need Required, not RequiresNew, to get the distributed transaction.
            using (var transaction = new TransactionScope(TransactionScopeOption.Required,
                new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted },
                TransactionScopeAsyncFlowOption.Enabled))
            using (var connection = new SqlConnection(ConnectionString))
            {
                await connection.OpenAsync();

                using (var command = connection.CreateCommand())
                {
                    command.CommandType = CommandType.Text;
                    command.CommandText = @"
SELECT a.transaction_type
FROM   sys.dm_tran_current_transaction c
INNER JOIN sys.dm_tran_active_transactions a ON c.transaction_id = a.transaction_id

-- Because this query is in a distributed transaction, if you want to throw, uncomment:
-- SAVE TRANSACTION Error
";

                    using (var reader = await command.ExecuteReaderAsync())
                    {
                        while (await reader.ReadAsync())
                        {
                            Console.WriteLine("Inner transaction_type is {0}", reader["transaction_type"]);
                        }
                    }
                }

                transaction.Complete();
            }
        }
    }
}

结果:

取消注释 SAVE TRANSACTION 与预期的添加异常一样,表示分布式事务。前面的 is_local 可以类似地测试。再一次,is_localtransaction_type 都不能可靠地指示分布式事务。

我一直无法找到另一种记录在案的方法来尝试在 SQL 中检测分布式事务。可能吗?如果可以,怎么做?

¹ 这个问题表面上与 .net detect distributed transaction 相关,但我需要从 SQL 而不是 .NET 进行检测。

² 我需要在不引起错误的情况下检测分布式事务,所以我不能只在查询中放入 SAVE TRANSACTION 并等待错误。

到目前为止,我找到的最佳解决方案是检查 sys.dm_tran_active_transactions 视图中的不同字段。 Documentation 描述列 transaction_uow:

Transaction unit of work (UOW) identifier for distributed transactions. MS DTC uses the UOW identifier to work with the distributed transaction.

对于我发现的每个案例,当我们在分布式事务中时,transaction_uow 是非空的;否则,transaction_uow 为空。下面SQL演示:

BEGIN TRANSACTION

SELECT IIF(a.transaction_uow IS NULL, N'Not Distributed', N'Distributed') AS [Distributed?]
FROM   sys.dm_tran_current_transaction c
INNER JOIN sys.dm_tran_active_transactions a ON c.transaction_id = a.transaction_id

ROLLBACK TRANSACTION

BEGIN DISTRIBUTED TRANSACTION

SELECT IIF(a.transaction_uow IS NULL, N'Not Distributed', N'Distributed') AS [Distributed?]
FROM   sys.dm_tran_current_transaction c
INNER JOIN sys.dm_tran_active_transactions a ON c.transaction_id = a.transaction_id

ROLLBACK TRANSACTION

结果:

修改问题中的 C# 代码以测试分布式事务时的行为相同。