为什么我的MSDTC事务没有在我的localhost环境中正确回滚?

时间:2017-03-03 16:55:56

标签: c# distributed-transactions msdtc

我目前正在尝试针对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();
}

初始化throw new Exception();后,第三个webapi项目已TransactionScope

要在我的机器上设置MSDTC,我已按照here所述的文章执行了以下操作:

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

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

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

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

    组件服务 - > Exapand Computers - >展开MyComputer - >展开分布式事务处理协调器 - >右键单击“本地DTC”并选择属性 - >选择“安全”选项卡

    这些是当前设置:

  4. enter image description here

    我一直在查看here所描述的一些故障排除信息。本文中的其中一个步骤介绍了如何下载DTCTool并运行RMClient.exe。我已经完成了这个并生成了以下日志:

      

    错误(RpcStatus = 1753):在RPCuser.cpp(行:116)RPC服务器过程   LogOn失败(1753)

         

    DEBUG TIP ::此错误表示客户端能够查询   指定目标计算机上端口135的端点映射器,但也可以   无法联系WinRM服务器。检查:

         
        
    1. 防火墙阻止更高端口 - 在客户端和服务器之间获取netmon跟踪并分析此行为的流量
    2.   
    3. 如果在服务器上启用了Windows防火墙,请检查WinRM.exe是否在例外列表中
    4.   
    5. 在群集环境中检查群集名称是否正在解析运行MSDTC / WinRM的节点
    6.   
    7. 在群集环境中,在客户端和MSDTC群集名称之间执行DTCPing测试,以查看客户端是否无法登录服务器
    8.   

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

    我不确定我是否要在我的应用程序中正确设置MSDTC。我刚刚从互联网上找到了我能找到的关于它的点点滴滴。对此有任何帮助将非常感激。

    谢谢

1 个答案:

答案 0 :(得分:0)

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

要总结链接,可以使用TransactionInterop将控制台应用程序中初始化的主事务检索为令牌,并作为请求标头或cookie的一部分传递给webapi:

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; 
            } 
        } 
    } 
}