WCF、MSMQ 和 2 个队列上的独立事务

WCF, MSMQ and independent transactions on 2 queues

我已经构建了一个处理 MSMQ 的 WCF 服务,我们称该服务为 QueueService。合同如下所示:

// Each call to the service will be dispatched separately, not grouped into sessions.
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class QueueServiceContract : IQueueServiceContract
{
    [OperationBehavior(TransactionScopeRequired = true)]
    public void QueueItems(List<Item> items)     // really should be called 'HandleQueueItems
    {
        //  Early in the processing I do:
        Transaction qTransaction = Transaction.Current;
        ...
        // I then check if the destination database is available.
        if(DB.IsAvailable)
            ... process the data
        else
            qTransaction.Rollback;
        ...
}

IQueueServiceContract 如下所示:

// The contract will be session-less.  Each post to the queue from the client will create a single message on the queue.
[ServiceContract(SessionMode = SessionMode.NotAllowed, Namespace = "MyWebService")]
public interface IQueueServiceContract
{
    [OperationContract(IsOneWay = true)]
    void QueueItems(List<Item> items);
}

队列服务 App.config 的相关部分如下所示。

<services>
  <service name="QueueService.QueueServiceContract">
    <endpoint address="net.msmq://localhost/private/MyQueueServiceQueue" binding="netMsmqBinding" contract="QueueService.IQueueServiceContract">
      <identity>
        <dns value="localhost" />
      </identity>
    </endpoint>
...
  <netMsmqBinding>
    <binding exactlyOnce="true" maxRetryCycles="1000" receiveRetryCount="1"
      retryCycleDelay="00:10:00" timeToLive="7.00:00:00" useActiveDirectory="false">
    </binding>
  </netMsmqBinding>

一切正常。当数据库不可用时,回滚会导致队列条目放入我已配置为每 10 分钟重试 7 天的重试子队列中。它的一切都有效,并且已经投入生产 6 个月左右。

现在我正在向服务添加日志记录。 QueueService 将把日志条目排队到我们称为的另一个队列中:LogQueue。要求是无论qTransaction回滚与否,都应该向LogQueue发送一条消息,指示请求的状态。

在 QueueService app.config 我添加了:

  <client>
  <endpoint address="net.msmq://localhost/private/MyLogQueue"
    binding="netMsmqBinding" bindingConfiguration="NetMsmqBinding_ILogContract"
    contract="LogServiceReference.ILogContract" name="NetMsmqBinding_ILogContract">
    <identity>
      <dns value="localhost" />
    </identity>
  </endpoint>
</client>
...
    <binding name="NetMsmqBinding_ILogContract" timeToLive="7.00:00:00">
      <security mode="None" />
    </binding>

在 LogService app.config 中,我有:

    <service name="LogService.LogContract">
    <endpoint address="net.msmq://localhost/private/MyLogQueue" binding="netMsmqBinding" contract="LogService.ILogContract">
      <identity>
        <dns value="localhost" />
      </identity>
    </endpoint>
... 
  <netMsmqBinding>
    <binding exactlyOnce="true" maxRetryCycles="1000" receiveRetryCount="1" retryCycleDelay="00:10:00" timeToLive="7.00:00:00"  useActiveDirectory="false">
    </binding>
  </netMsmqBinding>
...

然后,在 QueueItems 方法的末尾,我执行以下操作:

LogContractClient proxy = new LogContractClient();
proxy.LogTransaction(myLoggingInformation);             // This queues myLoggingInformation to the LogQueue.

这一切都很好......直到......数据库不可用并且事务被回滚。

回滚将在调用 proxy.LogTransaction 之前发生,我将得到:

System.ServiceModel.CommunicationException: 'An error occurred while sending to the queue: The transaction specified cannot be enlisted. (-1072824232, 0xc00e0058).Ensure that MSMQ is installed and running. If you are sending to a local queue, ensure the queue exists with the required access mode and authorization.'

如果我将 proxy.LogTransaction 移到 qTransaction.Rollback 之前,则日志条目永远不会放入 LogQueue。

我的工作理论是 WCF 将两个队列上的操作视为单个事务:从 QueueService 队列读取和写入 LogQueue。因此,如果我在回滚之后尝试写入 LogQueue,事务已经结束,但如果我在调用回滚之前写入 LogQueue,则对队列的写入也会回滚。

有什么方法可以在不同时回滚 LogService 事务的同时保留回滚 queueService 事务的能力?

我认为您可以通过在设置了 TransactionScopeOption.Suppress 的事务范围内包装对日志队列客户端的调用来解决此问题。这将强制操作在环境事务之外发生。

类似于:

using (var scope = new TransactionScope (TransactionScopeOption.Suppress))
{
    // Call to enqueue log message
}

你的理论很有道理。因为您使用的是事务队列,所以 WCF 通过在 DTC 事务中的消息处理程序中登记所有内容来确保事务的一致性。这显然包括日志消息的排队,并且如果不需要的话,这是预期的行为。