为什么我的 MSDTC 事务不能在我的本地主机环境中正确回滚?

Why is my MSDTC transaction not correctly rolling back on my localhost environment?

我目前正在尝试针对 SQL Server 2016 编写一个简单的 C# 应用程序以使用 Microsoft 分布式事务 (MSDTC)。在我尝试通过网络在服务器上测试它之前,我试图让它在我的本地机器 (Windows 10 Pro) 上运行。

.NET 解决方案由一个控制台应用程序项目和三个独立的 webapi 项目组成。控制台应用程序调用每个 webapi 项目,然后每个 webapi 项目将一个简单的记录写入数据库。这似乎工作正常。然后我想做的是让第三个 webapi 项目生成一个异常,并让所有以前的记录作为分布式事务的一部分回滚。但是,我的问题是前两个项目正在将它们的记录提交到数据库而不是回滚。

我设置它的方式(我相信它是正确的)是每个项目都使用 System.Transactions 来加入使用 DTC。

控制台应用程序充当根事务管理器并通过执行类似于以下内容的操作来启动事务:

using (TransactionScope scope = new TransactionScope())
{
    var orderId = Guid.NewGuid();
    ProcessSales(orderId);
    ProcessBilling(orderId);
    ProcessShipping(orderId);

    scope.Complete();
}

上面的每个 Process...() 方法都使用 HttpClient 调用其对应的 webapi 项目。

然后每个 webapi 项目都应该使用另一个事务在根事务中登记:

using (TransactionScope scope = new TransactionScope())
{
    string connectionString = @"Server=MyServerNameHere;Database=MyDatabaseNameHere;Trusted_Connection=True;";

    using (SqlConnection connection = new SqlConnection(connectionString))
    {
        string sql = "INSERT INTO dbo.Shipping (ShippingId, OrderId, Created) VALUES (@ShippingId, @OrderId, GetDate())";

        using (SqlCommand command = new SqlCommand(sql, connection))
        {
            ...
            command.ExecuteNonQuery();
        }
    }

    scope.Complete();
}

第三个 webapi 项目 throw new Exception(); 刚刚初始化 TransactionScope.

为了在我的机器上设置 MSDTC,我按照 here 中描述的文章进行了操作,并执行了以下操作:

  1. 已在管理工具 -> 服务中检查“分布式事务处理协调器”服务 运行。

  2. 已设置 Windows 防火墙以允许分布式事务:

    控制面板 -> Windows 防火墙 -> 允许应用程序或功能通过 Windows 防火墙 -> 将“分发事务协调器”添加到列表中。

  3. 在组件服务中启用网络事务:

    组件服务 -> 展开计算机 -> 展开我的电脑 -> 展开分布式事务处理协调器 -> 右键单击​​ 'Local DTC' 和 select 属性 -> Select 安全选项卡

    这些是当前设置:

我也一直在查看 here 中描述的一些故障排除信息。本文中的步骤之一描述了下载 DTCTool 和 运行 RMClient.exe。我这样做了,它生成了以下日志:

Error(RpcStatus=1753): at RPCuser.cpp (Line:116) RPC Server procedure LogOn failure (1753)

DEBUG TIP::This error means that the Client was able to query the Endpoint Mapper at port 135 on the target machine specified but either was not able to contact the WinRM server. Check for :

  1. Firewall blocking higher ports - Get a netmon trace between the client and server and analyze the traffic for this behavior
  2. If Windows firewall is enabled on server the check if WinRM.exe is in the exception list
  3. In Cluster environment check if the cluster name is resolving to the node where MSDTC/WinRM is running
  4. In cluster environment do a DTCPing test between the client and MSDTC cluster name to see if that works Client failed to log on server

我不确定这个日志是否相关,但我现在有点卡住了。我暂时关闭了防火墙,但它仍然不起作用,所以第 1 点和第 2 点应该不是问题。而且我没有使用集群环境,所以第 3 点和第 4 点应该不是问题。

我不确定是否要在我的应用程序中正确设置 MSDTC。我只是把我能从互联网上找到的零零碎碎的东西放在一起。如有任何帮助,我们将不胜感激。

谢谢

用户 Pankaj Kapare 提供的 link 对解决这个问题很有帮助。基本上,我实际上并没有将在控制台应用程序(根事务管理器)中初始化的事务传递给任何 Web api 应用程序(这应该是显而易见的)。因此,每个 Web api 应用程序都在初始化自己的事务,而不是加入现有事务。

总结 link,可以使用 TransactionInterop 作为令牌检索在控制台应用程序中初始化的主事务,并作为请求的一部分传递到网络api header或 cookie:

if (Transaction.Current != null) 
{ 
    var token = TransactionInterop.GetTransmitterPropagationToken(Transaction.Current); 
    request.Headers.Add("TransactionToken", Convert.ToBase64String(token));                 
}

在 Web api 服务器应用程序中,可以检索交易令牌并将其用于注册主交易。文章建议使用效果很好的动作过滤器:

public class EnlistToDistributedTransactionActionFilter : ActionFilterAttribute 
{ 
    private const string TransactionId = "TransactionToken"; 

    /// <summary> 
    /// Retrieve a transaction propagation token, create a transaction scope and promote  
    /// the current transaction to a distributed transaction. 
    /// </summary> 
    /// <param name="actionContext">The action context.</param> 
    public override void OnActionExecuting(HttpActionContext actionContext) 
    { 
        if (actionContext.Request.Headers.Contains(TransactionId)) 
        { 
            var values = actionContext.Request.Headers.GetValues(TransactionId); 
            if (values != null && values.Any()) 
            { 
                byte[] transactionToken = Convert.FromBase64String(values.FirstOrDefault()); 
                var transaction = TransactionInterop.GetTransactionFromTransmitterPropagationToken(transactionToken); 

                var transactionScope = new TransactionScope(transaction); 

                actionContext.Request.Properties.Add(TransactionId, transactionScope); 
            } 
        } 
    } 

    /// <summary> 
    /// Rollback or commit transaction. 
    /// </summary> 
    /// <param name="actionExecutedContext">The action executed context.</param> 
    public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext) 
    { 
        if (actionExecutedContext.Request.Properties.Keys.Contains(TransactionId)) 
        { 
            var transactionScope = actionExecutedContext.Request.Properties[TransactionId] as TransactionScope; 

            if (transactionScope != null) 
            { 
                if (actionExecutedContext.Exception != null) 
                { 
                    Transaction.Current.Rollback(); 
                } 
                else 
                { 
                    transactionScope.Complete(); 
                } 

                transactionScope.Dispose(); 
                actionExecutedContext.Request.Properties[TransactionId] = null; 
            } 
        } 
    } 
}