SQL Server代理事务已完成毒性消息异常

时间:2014-08-26 21:04:06

标签: c# sql-server service-broker sqltransaction

我在C#应用程序中使用2008 R2中的SQL Server Broker,并尝试处理SQL Server检测到有害消息并禁用目标队列的情况。

发生这种情况时,在我尝试接收消息时会抛出SqlException。那时,我正在使用的SqlTransaction似乎不再可以提交。

我将使用此tutorial来演示我的C#代码。

首先使用教程中的T-SQL代码创建必要的服务代理对象并发送消息,使其位于目标队列中。

CREATE MESSAGE TYPE
       [//AWDB/1DBSample/RequestMessage]
       VALIDATION = WELL_FORMED_XML;
CREATE MESSAGE TYPE
       [//AWDB/1DBSample/ReplyMessage]
       VALIDATION = WELL_FORMED_XML;
GO

CREATE CONTRACT [//AWDB/1DBSample/SampleContract]
      ([//AWDB/1DBSample/RequestMessage]
       SENT BY INITIATOR,
       [//AWDB/1DBSample/ReplyMessage]
       SENT BY TARGET
      );
GO

CREATE QUEUE TargetQueue1DB;

CREATE SERVICE
       [//AWDB/1DBSample/TargetService]
       ON QUEUE TargetQueue1DB
       ([//AWDB/1DBSample/SampleContract]);
GO

CREATE QUEUE InitiatorQueue1DB;

CREATE SERVICE
       [//AWDB/1DBSample/InitiatorService]
       ON QUEUE InitiatorQueue1DB;
GO

DECLARE @InitDlgHandle UNIQUEIDENTIFIER;
DECLARE @RequestMsg NVARCHAR(100);

BEGIN TRANSACTION;

BEGIN DIALOG @InitDlgHandle
     FROM SERVICE
      [//AWDB/1DBSample/InitiatorService]
     TO SERVICE
      N'//AWDB/1DBSample/TargetService'
     ON CONTRACT
      [//AWDB/1DBSample/SampleContract]
     WITH
         ENCRYPTION = OFF;

SELECT @RequestMsg =
       N'<RequestMsg>Message for Target service.</RequestMsg>';

SEND ON CONVERSATION @InitDlgHandle
     MESSAGE TYPE 
     [//AWDB/1DBSample/RequestMessage]
     (@RequestMsg);

SELECT @RequestMsg AS SentRequestMsg;

COMMIT TRANSACTION;
GO

接下来运行这个C#代码,它是一个控制台应用程序。

using System.Data.SqlClient;

namespace ServerConsoleApplication
{
    class Program
    {
        static SqlConnection conn = null;

        static void Main(string[] args)
        {
            conn = new SqlConnection("connection string");
            conn.Open();

            Receive(); // 1
            Receive(); // 2
            Receive(); // 3
            Receive(); // 4
            Receive(); // 5
            Receive(); // 6 - Poison Message exception invoked

            conn.Close();
        }

        static void Receive()
        {
            using (SqlTransaction tran = conn.BeginTransaction())
            {
                try
                {
                    using (SqlCommand waitCommand = conn.CreateCommand())
                    {
                        waitCommand.Transaction = tran;
                        waitCommand.CommandText = string.Format("WAITFOR (RECEIVE TOP (1) conversation_handle, convert(xml,message_body) FROM TargetQueue1DB), TIMEOUT 1000");

                        using (SqlDataReader reader = waitCommand.ExecuteReader())
                        {
                        }
                    }

                    // Rollback on purpose to cause the poison message
                    tran.Rollback();
                }
                catch (SqlException ex)
                {
                    if (ex.Number == 9617)
                    {
                        // Re-Enable the queue
                        using (SqlCommand enableCmd = conn.CreateCommand())
                        {
                            enableCmd.Transaction = tran;
                            enableCmd.CommandText = string.Format(@"ALTER QUEUE TargetQueue1DB WITH STATUS = ON");
                            enableCmd.ExecuteNonQuery();
                        }

                        System.Data.SqlTypes.SqlGuid handle = System.Data.SqlTypes.SqlGuid.Null;

                        // Pull the poison message off the queue
                        using (SqlCommand waitCommand = conn.CreateCommand())
                        {
                            waitCommand.Transaction = tran;
                            waitCommand.CommandText = string.Format("WAITFOR (RECEIVE TOP (1) conversation_handle, convert(xml,message_body) FROM TargetQueue1DB), TIMEOUT 1000");

                            using (SqlDataReader reader = waitCommand.ExecuteReader())
                            {
                                while (reader.Read())
                                {
                                    handle = reader.GetSqlGuid(0);
                                }
                            }
                        }

                        // End the conversation just for clean up
                        using (SqlCommand endCmd = conn.CreateCommand())
                        {
                            endCmd.Transaction = tran;
                            endCmd.CommandText = "End Conversation @handle";
                            endCmd.Parameters.Add("@handle", System.Data.SqlDbType.UniqueIdentifier);
                            endCmd.Parameters["@handle"].Value = handle;
                            endCmd.ExecuteNonQuery();
                        }

                        // Commit the transaction so the message is removed from queue.
                        tran.Commit();
                    }
                }
            }
        }
    }
}

上面的代码只是对行为的演示。当然,你通常不会像这样接收和调用Rollback。

Receive方法接收消息并在事务上调用Rollback以刺激有害消息行为。在第六次调用时,抛出了接收SQLException,因为队列已按预期被禁用。

此时我想重新启用队列,关闭毒药消息并结束对话(不需要结束)。这一切都有效,但后来我提交了事务,因为我真的想要从队列中删除该有害消息。

结果 在提交调用声明

时抛出异常
  

此SqlTransaction已完成;它不再可用了。

如果没有在第6次调用Receive时调用Rollback或Commit,这个事务是如何完成的?

此外,TargetQueue1DB中的消息是如何被删除的?我认为接收不会从队列中删除消息,除非它是在已提交的事务中。但是,如果在调用该提交之前查看TargetQueue1DB,则队列为空。

如果稍微修改代码以便在捕获SqlException时waitCommand在范围内,您将看到waitCommand实例的Connection和Transaction属性已设置为null。这对我来说是个奇怪的行为。

1 个答案:

答案 0 :(得分:2)

SqlTransaction的客户端状态不一定反映服务器上的事务状态。考虑一下你捕到的异常是1205,死锁。在这种情况下,在服务器中引发异常之前,事务已在服务器上回滚,即使您当前帧中的SqlTransaction对象既未提交也未回滚。 / p>

在catch块中,您需要处理当前的事务对象并启动一个新的事务对象来执行错误处理逻辑。

消息已被删除,因为您在服务器上启动了实际事务时执行了catch处理逻辑。您使用了不再相关的过期tran对象。你的RECEIVE立刻得到了承诺(根本没有周围的交易)。