下面看看如何通过Service Broker功能来实现这一方案。
定义基础架构
实现author、发布者和不同的订阅者作为Service Broker服务。AuthorService通过与PublisherService开始一个新的会话(conversation)发送article 消息。在AuthorService发送一个实际的article消息之前,它需要事先通知PublisherService在会话中article的主 体(Subject)。
订阅者也是Service Broker服务,它将与PublisherService进行会话。订阅者首先发送请求感兴趣的主题,随后接受所有关于这一主题的已发布的 article消息。因为这些要求,PublisherService接口需要支持如下2个contracts:
l author服务发送 article消息的contract
l 订阅者订阅主题和接收已发 布article消息的contract
下面定义了相关的Service Broker对象。
消息类型Message Type
CREATE MESSAGE TYPE [http://ssb.csharp.at/SSB_Book/c10/PublishMessage]
VALIDATION = WELL_FORMED_XML;
GO
CREATE MESSAGE TYPE [http://ssb.csharp.at/SSB_Book/c10/ArticleMessage]
VALIDATION = NONE;
GO
CREATE MESSAGE TYPE [http://ssb.csharp.at/SSB_Book/c10/SubscribeMessage]
VALIDATION = WELL_FORMED_XML;
GO
契约Contract
CREATE CONTRACT [http://ssb.csharp.at/SSB_Book/c10/PublishContract]
(
[http://ssb.csharp.at/SSB_Book/c10/PublishMessage] SENT BY INITIATOR,
[http://ssb.csharp.at/SSB_Book/c10/ArticleMessage] SENT BY INITIATOR
)
GO
CREATE CONTRACT [http://ssb.csharp.at/SSB_Book/c10/SubscribeContract]
(
[http://ssb.csharp.at/SSB_Book/c10/SubscribeMessage] SENT BY INITIATOR,
[http://ssb.csharp.at/SSB_Book/c10/ArticleMessage] SENT BY TARGET
)
GO
队列 QUEUE 和 服务 SERVICE
发布者队列及其服务
CREATE QUEUE [PublisherQueue]
GO
CREATE SERVICE [PublisherService] ON QUEUE [PublisherQueue]
(
[http://ssb.csharp.at/SSB_Book/c10/PublishContract],
[http://ssb.csharp.at/SSB_Book/c10/SubscribeContract]
)
GO
订阅者队列及其服务
CREATE QUEUE SubscriberQueue1;
GO
CREATE SERVICE SubscriberService1 ON QUEUE SubscriberQueue1;
GO
CREATE QUEUE SubscriberQueue2;
GO
CREATE SERVICE SubscriberService2 ON QUEUE SubscriberQueue2;
GO
Author队列及其服务
CREATE QUEUE AuthorQueue;
GO
CREATE SERVICE AuthorService ON QUEUE AuthorQueue;
GO
在创建好上述Service Broker基础架构后,下面创建2个表,分别用来存储发布和订阅记录。针对本示例,我们分别创建如下2个表:Publications和 Subscriptions。(EntLib.com译 者注:Publications 表需要分别在Author和Publisher端创建;Subscriptions表需要分别在Publisher和Subscriber端创建。)
CREATE TABLE Publications
(
Publication UNIQUEIDENTIFIER NOT NULL PRIMARY KEY,
Subject NVARCHAR(MAX) NOT NULL,
OriginalXml XML NOT NULL
)
GO
CREATE TABLE Subscriptions
(
Subscriber UNIQUEIDENTIFIER NOT NULL PRIMARY KEY,
Subject NVARCHAR(MAX) NOT NULL,
OriginalXml XML NOT NULL
)
GO
通过这2个表,我们可以匹配来自AuthorService的article消息和订阅者。通过Subject列来连接这2个表,后面的“发布 者逻辑”中具体介绍。下面看看PublisherService端运行的服务程序。
应用发布者逻辑
在存储过程sp_PublisherService 中实现PublisherService服务的入口点(entry point)。当一有新的消息到达PublisherQueue队列是,sp_PublisherService存储过程自动激活,并开始处理消息。这一 存储过程能够处理如下消息类型:
[http://ssb.csharp.at/SSB_Book/c10/PublishMessage]: 在存储过程开始发布article消息时,它需要从AuthorService接收该消息类型。这一消息包含有如下article消息相同的主题 (Subject)。
[http://ssb.csharp.at/SSB_Book/c10/SubscribeMessage]: 在存储过程准备订阅一个主题时,它需要从订阅者服务方接收该消息类型。这一消息包含有订阅者请求的主题。
[http://ssb.csharp.at/SSB_Book/c10/ArticleMessage]: 在存储过程发布article消息时,它会从AuthorService方接收这一消息类型。
[http://schemas.microsoft.com/SQL/ServiceBroker/EndDialog]: 在存储过程准备关闭PublisherService打开的会话时,存储过程会从AuthorService或订阅方服务接收这一消息类型。
[http://schemas.microsoft.com/SQL/ServiceBroker/Error]: 在请求的主题不存在是,存储过程将从PublisherService接收这一消息类型。
下面的script显示sp_PublisherService存储过程如何处理这些消息类型。
sp_PublisherService 服务处理程序:
CREATE PROCEDURE sp_PublisherService
AS
BEGIN
DECLARE @Conversation UNIQUEIDENTIFIER;
DECLARE @Message VARBINARY(MAX);
DECLARE @MessageTypeName SYSNAME;
BEGIN TRANSACTION;
WAITFOR
(
RECEIVE TOP(1)
@Conversation = conversation_handle,
@Message = message_body,
@MessageTypeName = message_type_name
FROM PublisherQueue
), TIMEOUT 1000;
WHILE (@Conversation IS NOT NULL)
BEGIN
IF (@MessageTypeName = 'http://ssb.csharp.at/SSB_Book/c10/PublishMessage')
BEGIN
EXEC sp_ProcessPublicationRequest @Conversation, @Message;
END
ELSE IF (@MessageTypeName ='http://ssb.csharp.at/SSB_Book/c10/SubscribeMessage')
BEGIN
EXEC sp_ProcessSubscriptionRequest @Conversation, @Message;
END
ELSE IF (@MessageTypeName ='http://ssb.csharp.at/SSB_Book/c10/ArticleMessage')
BEGIN
EXEC sp_SendOnPublication @Conversation, @Message;
END
ELSE IF (@MessageTypeName IN (
N'http://schemas.microsoft.com/SQL/ServiceBroker/Error',
N'http://schemas.microsoft.com/SQL/ServiceBroker/EndDialog'))
BEGIN
END CONVERSATION @Conversation;
IF (EXISTS (SELECT * FROM Publications WHERE Publication = @Conversation))
BEGIN
EXEC sp_RemovePublication @Conversation;
END
IF (EXISTS (SELECT * FROM Subscribers))
BEGIN
EXEC sp_RemoveSubscriber @Conversation;
END
END
ELSE
BEGIN
-- Unexpected message
RAISERROR (N'Received unexpected message type: %s', 16, 1,
@MessageTypeName);
ROLLBACK;
RETURN;
END
COMMIT;
SELECT @Conversation = NULL;
BEGIN TRANSACTION;
WAITFOR
(
RECEIVE TOP(1)
@Conversation = conversation_handle,
@Message = message_body,
@MessageTypeName = message_type_name
FROM PublisherQueue
), TIMEOUT 1000;
END
COMMIT;
END
GO
在上面的存储过程中,先从PublisherQueue队列中接收一个新的消息。如下消息类型为[http://ssb.csharp.at /SSB_Book/c10/PublishMessage],就调用sp_ProcessPublicationRequest 存储过程,将接收到的发布数据记录到Publications表中。如果为[http://ssb.csharp.at/SSB_Book/c10 /SubscribeMessage] 消息类型,则调用sp_ProcessSubscriptionRequest 存储过程,负责将接收到的订阅数据记录到Subscriptions表中。最后,如果为 [http://ssb.csharp.at/SSB_Book/c10/ArticleMessage]消息类型,则 sp_PublisherService存储过程负责调用sp_SendOnPublication 分发该消息给所有匹配的订阅者。
通过sp_ProcessPublicationRequest 和sp_ProcessSubscriptionRequest 存储过程来管理发布和订阅记录。这2个存储过程分别调用其他的存储过程,将接收到的消息插入到Publications 或 Subscriptions 表中。如下显示 the sp_ProcessPublicationRequest 存储过程。因为sp_ProcessSubscriptionRequest 存储过程比较相似,这里忽略该存储过程。
sp_ProcessPublicationRequest 存储过程脚本:
CREATE PROCEDURE sp_ProcessPublicationRequest
@Conversation UNIQUEIDENTIFIER,
@Message VARBINARY(MAX)
AS
BEGIN
DECLARE @Request XML;
DECLARE @Subject NVARCHAR(MAX);
SELECT @Request = CAST(@Message AS XML);
WITH XMLNAMESPACES (DEFAULT 'http://ssb.csharp.at/SSB_Book/c10/PublishSubscribe')
SELECT @Subject = @Request.value(N'(//Publish/Subject)[1]', N'NVARCHAR(MAX)');
IF (@Subject IS NOT NULL)
BEGIN
EXEC sp_PublishPublication @Conversation, @Subject, @Message;
END
ELSE
BEGIN
END CONVERSATION @Conversation
WITH ERROR = 1
DESCRIPTION = N'The publication is missing a subject';
EXEC sp_RemovePublication @Conversation;
END
END
GO
sp_ProcessPublicationRequest存储过程调用sp_PublishPublication,并传入 @Conversation, @Subject, @Message 三个参数。如下是sp_PublishPublication 存储过程。
sp_PublishPublication 存储过程脚本:
CREATE PROCEDURE sp_PublishPublication
@Publication UNIQUEIDENTIFIER,
@Subject NVARCHAR(MAX),
@OriginalXml XML
AS
BEGIN
INSERT INTO Publications (Publication, Subject, OriginalXml)
VALUES
(
@Publication,
@Subject,
@OriginalXml
)
END
GO
Publications 表中的Publication列和Subscriptions表中的Subscription 列都存放会话ID,你需要这些会话ID来发送article消息到订阅方。最后一个存储过程是sp_SendOnPublication,在有 [http://ssb.csharp.at/SSB_Book/c10/ArticleMessage] 消息从 AuthorService接收到后,调用才存储过程。
sp_SendOnPublication 存储过程脚本:
CREATE PROCEDURE sp_SendOnPublication
@Publication UNIQUEIDENTIFIER,
@Article VARBINARY(MAX)
AS
BEGIN
DECLARE @Subscription UNIQUEIDENTIFIER;
DECLARE @cursorSubscriptions CURSOR;
SET @cursorSubscriptions = CURSOR LOCAL SCROLL FOR
SELECT Subscriber
FROM Subscriptions s
JOIN Publications p ON s.Subject = p.Subject
WHERE p.Publication = @Publication;
BEGIN TRANSACTION;
OPEN @cursorSubscriptions;
FETCH NEXT FROM @cursorSubscriptions
INTO @Subscription;
WHILE (@@fetch_status = 0)
BEGIN
IF (@Article IS NOT NULL)
BEGIN
SEND ON CONVERSATION @Subscription
MESSAGE TYPE [http://ssb.csharp.at/SSB_Book/c10/ArticleMessage] (@Article);
END
ELSE
BEGIN
SEND ON CONVERSATION @Subscription
MESSAGE TYPE [http://ssb.csharp.at/SSB_Book/c10/ArticleMessage];
END
FETCH NEXT FROM @cursorSubscriptions INTO @Subscription;
END
CLOSE @cursorSubscriptions;
DEALLOCATE @cursorSubscriptions;
COMMIT;
END
GO
sp_SendOnPublication 存储过程使用一个cusor,发送从AuthorService接收到的article消息,给匹配的订阅者。
SEND ON CONVERSATION @Subscription
MESSAGE TYPE [http://ssb.csharp.at/SSB_Book/c10/ArticleMessage] (@Article);
通过对Publications 和 Subscriptions 表的Subject 字段进行连接,进行匹配:
SELECT Subscriber
FROM Subscriptions s
JOIN Publications p ON s.Subject = p.Subject
WHERE p.Publication = @Publication;
当接收到的是[http://schemas.microsoft.com/SQL/ServiceBroker/EndDialog] 消息类型时(从AuthorService或从订阅者服务方),相应的发布或订阅数据则需要从Publications或Subscriptions表中 删除。分别通过sp_RemovePublication 或 sp_RemoveSubscriptions 存储过程来实现。
sp_RemovePublication 存储过程脚本:
CREATE PROCEDURE sp_RemovePublication
@Publication UNIQUEIDENTIFIER
AS
BEGIN
DELETE FROM Publications
WHERE Publication = @Publication
END
GO发布信息Publishing Information
这一节演示订阅者通过订阅从PublisherService服务接收信息,已经AuthorService如何发送新的article消息到 PublisherService服务进行分发。在订阅者可以从PublisherService接收新的article消息之前,它必须请求一个订阅。 可以通过发送一个 [http://ssb.csharp.at/SSB_Book/c10/SubscribeMessage] 消息来实现,代码如下:
订阅方请求订阅脚本:
DECLARE @ch UNIQUEIDENTIFIER;
BEGIN DIALOG CONVERSATION @ch
FROM SERVICE [SubscriberService1]
TO SERVICE 'PublisherService'
ON CONTRACT [http://ssb.csharp.at/SSB_Book/c10/SubscribeContract]
WITH ENCRYPTION = OFF;
SEND ON CONVERSATION @ch
MESSAGE TYPE [http://ssb.csharp.at/SSB_Book/c10/SubscribeMessage]
(
N'<?xml version="1.0"?>
<Request xmlns="http://ssb.csharp.at/SSB_Book/c10/PublishSubscribe">
<Subject>Subject1</Subject>
</Request>'
);
GO
在上述脚本中,你必须在发送的消息中指定消息主题,随后会根据主题从PublisherService 中获取发布数据。在订阅者一设置好订阅,AuthorService就可以发送article消息给PublisherService进行分发。示例代码 如下:
[AuthorService] 发送PublishMessage消 息:
DECLARE @ch UNIQUEIDENTIFIER;
BEGIN DIALOG CONVERSATION @ch
FROM SERVICE [AuthorService]
TO SERVICE 'PublisherService'
ON CONTRACT [http://ssb.csharp.at/SSB_Book/c10/PublishContract]
WITH ENCRYPTION = OFF;
SEND ON CONVERSATION @ch
MESSAGE TYPE [http://ssb.csharp.at/SSB_Book/c10/PublishMessage]
(
N'<?xml version="1.0"?>
<Publish xmlns="http://ssb.csharp.at/SSB_Book/c10/PublishSubscribe">
<Subject>Subject1</Subject>
</Publish>'
);
AuthorService必须指定随后的article消息属于哪一主题,它通过发送[http://ssb.csharp.at /SSB_Book/c10/PublishMessage] 消息类型,通知PublisherService 记录相应的发布记录。最后,AuthorService 发送属于前面指定主题的、不同的article消息。
发送特定主题的article消息:
SEND ON CONVERSATION @ch
MESSAGE TYPE [http://ssb.csharp.at/SSB_Book/c10/ArticleMessage]
(
N'This is an article on Subject1'
);
SEND ON CONVERSATION @ch
MESSAGE TYPE [http://ssb.csharp.at/SSB_Book/c10/ArticleMessage]
(
N'And this is another article on Subject1'
);
GO
在AuthorService 一发送article消息到PublisherService,这些消息将通过sp_PublisherService存储过程自动转发给订阅者。现在, 你可以查询订阅者服务对应的队列,⑾址⑺偷腶rticle消息成功转发过来了。
备注:因为Service Broker内在的异步处理特性,在你查看SubscriberQueue1和SubscriberQueue2队列之前,你需要等待几秒钟,直到这些消 息已经处理完成。