【问题标题】:SQL Server lock issue (distributed transactions with WCF)SQL Server 锁定问题(使用 WCF 的分布式事务)
【发布时间】:2012-04-19 15:27:46
【问题描述】:

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

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

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

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

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

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

我看到数据库 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 事务可能不是一个好主意。

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

【问题讨论】:

  • @JoshuaDrake 您好,感谢您的链接,但我实际上没有并发问题。我确实有多个线程,但每个线程都在查询其“自己的”数据库。我什至不会同时在同一个数据库上运行多个事务:一切都是同步运行的,所以我一次只执行一个完整的事务(客户端读取/更新 DB A,然后将数据发送到服务器,然后服务器写入DB B,最后客户端从 A 中删除)。

标签: c# sql-server wcf soap transactions


【解决方案1】:

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

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

即使您坚持使用 WCF,使用协调事务也不是正确的方法。您应该使用 MSMQ 排队通道并以面向消息的方式设计它,而不是 RPC 调用样式(并且通过 http WCF 绑定的 SOAP/HTTP 调用只不过是一种美化的 RPC)。我强烈推荐你阅读The Game of Distributed Systems Programming .您现在处于第 1 级,您需要过渡到第 2 级。WCF HTTP 不会将您带到那里。带有 MSMQ 绑定的 WCF 可能,但您很快就会意识到编写队列通道与编写 RPC 通道没什么,而且您必须从头开始重写几乎所有内容无论如何。如果您愿意放弃Kool Aid,SSB 可以用更可靠、更快的车辆带您到那里。

【讨论】:

  • 感谢您的回答。实际上,出于互操作性的原因,我不能使用事务复制、服务代理或 MSMQ。请查看我更新的问题(对不起,问题的第一个版本可能不够准确)。我真的被 WCF+SOAP 困住了。
猜你喜欢
  • 2011-05-20
  • 2016-06-05
  • 2012-08-14
  • 2019-06-30
  • 1970-01-01
  • 1970-01-01
  • 2014-10-07
  • 1970-01-01
相关资源
最近更新 更多