3

我遇到了分布式事务的问题。

我正在使用 SQL Server 2008 R2、Windows 7、.Net 4.0。

这就是我真正想做的事情:

  • 我有一个缓冲数据库(名为 A)
  • 我有另一个数据库(名为 B)
  • 我想通过 WCF Web 服务将数据从数据库 A 发送到数据库 B(SOAP over HTTP,A 和 B 不在同一台机器上)
  • 如果数据成功发送到数据库 B,则从数据库 A 中删除数据

请注意,这是对我目前遇到的问题的非常简化的描述。在我的实际应用程序中,我不会在多个 SQL Server 实例之间复制数据(请参阅帖子末尾的更新)。

一切都是事务的一部分,因此整个操作是原子的。

这是我目前正在尝试按顺序执行的操作(事务中的所有内容):

  1. 我从数据库 A 中读取了一个块(比如 50 行)
  2. 我更新了在步骤 1 中读取的行(我实际上将行的布尔Sent列设置为 value true,所以将来我知道这些行已发送)
  3. 我使用(客户端)一个 WCF Web 服务(SOAP 1.2,wsHttpBinding),它是事务流(阻塞同步调用)的一部分。webservice请求发送步骤1中读取的数据
  4. webservice 实现(服务器端)在数据库 B 中插入数据(包含在 webservice 请求中的数据)
    • 如果没有从服务器接收到异常,则从数据库 A 中删除Sent值为 as的数据true
    • FaultException如果从服务器接收到特定Sent值,则从数据库 A 中删除值为 true 的数据
    • 对于其他FaultExceptions和其他异常(未找到端点或其他任何情况),Sent值为 astrue的数据不会从数据库 A 中删除

注意:实际上有多个缓冲区数据库(A1、A2、A3...、AN)和多个目标数据库(B1、...BN)。有 N 个线程来处理 N 个数据库。

如果我运行我的服务器和客户端,一切正常。数据是从数据库 A 到数据库 B 的每个块原子地“传输”的。当我在事务中间粗暴地停止我的客户端(进程被杀死)时,大多数时候一切都很好(即数据没有从数据库 A 中删除或添加到数据库 B)。

但有时(这是我的实际问题),我的数据库 B 被锁定。解锁它的唯一选择是终止服务器进程。

当问题发生时,我可以在 MSDTC 中看到有一个活动事务(请注意,在屏幕截图中有 3 个事务,因为我目前正在处理 3 个缓冲区数据库和 3 个目标数据库,但有时即使只有 1 个事务有 3 个数据库)。

在此处输入图像描述

我看到数据库 B 在尝试再次运行数据库 B 时被锁定SELECT。如下图所示,我的SELECT请求被会话 ID 67 阻止,该会话 ID 对应于INSERT服务器进程在数据库 B 中的一个。

在此处输入图像描述

锁定永远保持(没有事务超时,即使在 1 小时后),直到服务器进程终止。我无法在 MSDTC 中验证或取消交易,我收到一条警告说“ it cannot be aborted because it is not "In Doubt"”。

为什么数据库 B 保持锁定状态?如果客户端终止,事务不应该失败并且数据库B上的锁在超时后被释放吗?

这是我的服务器端代码:

// Service interface
[ServiceContract]
public interface IService
{
    [OperationContract]
    [FaultContract(typeof(MyClass))]
    [TransactionFlow(TransactionFlowOption.Allowed)]
    void SendData(DataClass data);
}

// Service implementation
[ServiceBehavior()]
public partial class Service : IService
{
    [OperationBehavior(TransactionScopeRequired = true)]
    public void SendData(DataClass data)
    {
        if (data == null)
        {
            throw new FaultException<MyClass>(new MyClass());
        }

        try
        {
            // Inserts data in database B
            using (DBContextManagement ctx = new DBContextManagement())
            {
                // Inserts data using Entity Framework
                // This will add some entities to the context
                // then call context.SaveChanges()
                ctx.InsertData(data);
            }
        }
        catch (Exception ex)
        {
            throw new FaultException<MyClass>(new MyClass());
        }
    }
}

这是我的服务器端配置(自托管 WCF 服务):

<system.serviceModel>
    <services>
      <service name="XXXX.MyService">
        <endpoint binding="wsHttpBinding" bindingConfiguration="WsHttpBinding_IMyService" contract="XXXX.IMyService" />
      </service>
    </services>
    <bindings>
      <wsHttpBinding>
        <binding name="WsHttpBinding_IMyService" transactionFlow="true" allowCookies="true" >
          <readerQuotas maxDepth="32" maxArrayLength="2147483647" maxStringContentLength="2147483647" />
          <security mode="None" />
        </binding>
      </wsHttpBinding>
    </bindings>
    <behaviors>
      <serviceBehaviors>
        <behavior name="">
          <serviceTimeouts transactionTimeout="00:00:20" />
          <serviceMetadata httpGetEnabled="true" />
          <serviceDebug includeExceptionDetailInFaults="true" />
          <dataContractSerializer maxItemsInObjectGraph="2147483647" />
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <serviceHostingEnvironment multipleSiteBindingsEnabled="true" />
  </system.serviceModel>

这是我的客户代码:

try
{
    using (DBContextManagement ctx = new DBContextManagement())
    {
        using (TransactionScope ts = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions { IsolationLevel = System.Transactions.IsolationLevel.ReadUncommitted, Timeout = new TimeSpan(0, 0, 30) }, EnterpriseServicesInteropOption.None))
        {
            // First of all, retrieves data from database A
            // Internally queries the database A through Entity Framework
            var data = ctx.GetData();

            // Mark data as sent to the server
            // Internally updates the database A through Entity Framework
            // This actually set the Sent property as true, then call
            // context.SaveChanges()
            ctx.SetDataAsSent(data);

            try
            {
                // Send data to the server
                MyServiceClient proxy = new MyServiceClient();
                MyServiceClient.SendData(data);

                // If we're here, then data has successfully been sent
                // This internally removes sent data (i.e. data with
                // property Sent as true) from database A through entity framework
                // (entities removed then context.SaveChanges)
                ctx.RemoveSentData();
            }
            catch (FaultException<MyClass> soapError)
            {
                // SOAP exception received
                // We internally remove sent data (i.e. data with
                // property Sent as true) from database A through entity framework
                // (entities removed then context.SaveChanges)
                ctx.RemoveSentData();
            }
            catch (Exception ex)
            {
                // Some logging here
                return;
            }

            ts.Complete();
        }
    }
}
catch (Exception ex)
{
    // Some logging here (removed from code)
    throw;
}

这是我的客户端配置:

<system.serviceModel>
    <bindings>
      <wsHttpBinding>
        <binding name="WsHttpBinding_IMyService"
                 closeTimeout="00:01:00"
                 openTimeout="00:01:00"
                 receiveTimeout="00:10:00"
                 sendTimeout="00:01:00"
                 allowCookies="false"
                 bypassProxyOnLocal="false"
                 hostNameComparisonMode="StrongWildcard"
                 transactionFlow="true"
                 maxBufferPoolSize="2147483647"
                 maxReceivedMessageSize="2147483647"
                 messageEncoding="Text"
                 textEncoding="utf-8"
                 useDefaultWebProxy="true">

          <readerQuotas maxDepth="32"
                        maxStringContentLength="2147483647"
                        maxArrayLength="16384"
                        maxBytesPerRead="4096"
                        maxNameTableCharCount="16384" />
          <security mode="None">
            <transport clientCredentialType="None" proxyCredentialType="None" realm="" />
            <message clientCredentialType="UserName" algorithmSuite="Default" />
          </security>
        </binding>
      </wsHttpBinding>
    </bindings>

    <client>
      <endpoint address="http://localhost:8080/MyService.svc"
                binding="wsHttpBinding"
                bindingConfiguration="WsHttpBinding_IMyService"
                contract="XXX.IMyService"
                name="WsHttpBinding_IMyService" />
    </client>

  </system.serviceModel>

所以我的问题是:

  • 我的分布式事务模式/设计是有效且健壮的设计吗?
  • 如果我的客户的事务被残酷地终止,什么会导致我的数据库 B 被无限期锁定?
  • 我应该更改什么(配置和/或代码和/或设计)以使其按预期工作(即,如果我的客户端进程死亡/崩溃,我希望事务被中止,以便数据库 B 不被锁定)?

谢谢。


编辑

我确实认为我过度简化了对实际用例的描述。看起来我实际上是在多个 SQL Server 实例之间进行简单的数据复制。这没有很好的解释(我的错),这不是我想要达到的目标。

我不是简单地复制数据。我从机器 M1 上的 A 读取,然后写入机器 M2 上的 B,但是写入的不是我读过的,而是来自读取的一些计算值。我很确定 SQL Server 复制服务可以为我处理业务计算,但由于某些原因我不能这样做:

  • 我不能使用 SQL Server 复制,因为我实际上不负责服务器端(写入数据库 B)。我什至不确定另一边是否会有 SQL Server(可能是带有 MySQL、PostgreSQL 或任何东西的 Java 后台)
  • 出于同样的原因(可能是异构数据库和环境),我不能使用 SQL Server 服务代理或任何面向消息的中间件(适合 IMO)
  • 我无法更改 Web 服务,因为现在已经定义并修复了接口。

我被 WCF 困住了,由于互操作性要求,我什至无法更改绑定配置以使用 MSMQ 或其他任何东西。MSMQ 肯定很棒(我已经在项目的另一部分使用它),但它只是 Windows。SOAP 1.2 是标准协议,SOAP 1.2 事务也是标准的(WS-Atomic实现)。

实际上,也许 WCF 事务不是一个好主意。

如果我正确理解了它的实际工作原理(如果我错了,请纠正我),它只允许在服务器端“继续”事务范围,这将需要在服务器端配置事务协调器,这可能会中断我的互操作性需求(同样,服务器端可能有一个与事务协调器没有很好集成的数据库)。

4

1 回答 1

2

也许这不是您要寻找的答案,但恕我直言,您只是在重新发明轮子。您需要一种将更改从 A 复制到 B 的可靠方法。已经有一个内置的解决方案,即Transactional Replication。只需将其配置为不复制删除,您就拥有了您所要求的语义。

对于事务复制不符合要求的事情(您描述的复杂拓扑,有许多发布者和许多订阅者,可能就是这种情况)我仍然不会使用 WCF。SQL Server 已经拥有可靠的消息传递,该消息传递速度极快,具有事务性,并且明确设计为避免分布式事务和两阶段提交:Service Broker

即使您坚持使用 WCF,使用协调事务也不是正确的方法。您应该使用 MSMQ 排队通道并以面向消息的方式设计它,而不是 RPC 调用样式(并且通过 http WCF 绑定的 SOAP/HTTP 调用只不过是一种美化的 RPC)。我强烈建议您阅读分布式系统编程游戏 。您现在处于第 1 级,您需要过渡到第 2 级。WCF HTTP 不会将您带到那里。带有 MSMQ 绑定的 WCF 可能,但是您很快就会意识到,对排队通道进行编程与对 RPC 通道进行编程完全不同,而且您必须从头开始重写几乎所有内容。如果您愿意放弃Kool Aid ,SSB 可以用更可靠、更快的车辆带您到那里.

于 2012-04-19T17:07:56.433 回答