发布时间:2014-08-01 00:00 来源:未知
一、引言
SQL Server 2005 的一个主要成就是可以实现可靠、可扩展且功能完善的数据库应用程序。与 .NET Framework 2.0 公共语言运行库 (CLR) 的集成使开发人员可以将重要的业务逻辑与存储过程合并,而 T-SQL 和 XML 中的新增功能扩展了数据操作的可用范围以及开发人员可用的存储功能。另一个重要功能是 SQL Server Service Broker,它为数据库应用程序增加了可靠、可扩展、分布式异步功能。
二、为什么要编写异步排队操作?在开发 SQL Server 2005 时,我们与成功开发过大型可扩展数据库应用程序的人员进行了交谈。结果发现他们几乎所有的应用程序中都有一个或多个操作是以异步排队方式执行的。股票交易系统中的结算活动是排队的,这样可以在后台进行结算,在前端处理其他交易。订单输入系统中的发货信息放在一个排队中,稍后将由另一台服务器(通常位于其他位置)上运行的发货系统读取。旅行预订系统在客户填写完路线后再进行实际的预订,并在预订完成后发送确认电子邮件。在所有这些示例中,许多工作都是通过异步方式完成的,从而提高了系统的响应速度,因此用户无须等待工作完成就可以收到响应。
在大多数大型系统中,经过仔细分析后都可以发现,许多处理都可以通过异步方式完成。虽然应用程序中的排队任务无须立即完成,但系统必须确保这些任务能够可靠地完成。Service Broker 使异步排队任务的执行可靠并且易于实现。
使应用程序中的部分任务异步执行的另一个优势是这些任务的处理工作可以在资源可用时完成。假如订单输入系统中的发货任务可以从队列中执行,发货系统就无需具有处理峰值订单负载的能力。在许多订单输入系统中,峰值订单率可能是平均订单率的两倍或三倍。由于订单输入系统必须具有处理峰值负载的能力,因此大量处理能力在很大一部分时间内都处于闲置状态。假如在出现峰值时对异步任务进行排队并在空闲时执行,将显著提高系统的利用率。
三、为什么排队应用程序难于编写?既然异步排队应用程序具有许多优势,为什么不以这种方式编写所有应用程序?这是因为编写异步排队应用程序相当困难!许多尝试在应用程序中使用数据库表作为队列的开发人员发现,实际操作远比想象的困难。作为队列的表中包含多个进程,包括同时插入、读取和删除几条记录。这将带来并发问题、降低系统性能并导致频繁死机。尽管许多开发人员都成功地解决了这些难题,但实际操作却非常困难。Service Broker 通过使队列成为 SQL Server 2005 数据库中的第一级数据库对象解决了这些问题。编写队列的大多数难题都已解决,因此开发人员可以专注于编写异步应用程序,而不是编写排队基础结构。本文的其余内容将讨论编写排队应用程序涉及到的难题,并解释 Service Broker 如何帮助您解决这些难题。
在许多异步排队应用程序中,排队的消息实际上是有价值的业务对象。例如在订单输入系统中,假如将发货信息放入队列中以便稍后处理,那么丢失排队的数据将意味着无法按订单发货。许多可靠的消息传送系统都将消息存储在文件系统中,因此磁盘驱动器的损坏将导致消息丢失。Service Broker 将消息存储在隐藏的数据库表中,因此 SQL Server 为保护数据而提供的所有数据完整性功能同样可用于保护排队的消息。假如使用数据库镜像进行故障恢复,当因数据库故障而转移到第二个数据库后,所有消息都会转移到第二个数据库中,这样 Service Broker 应用程序将继续运行而不会丢失任何数据。
多读取器队列是一种扩展异步应用程序的最有效的方式。为证明这一点,我将通过大家都熟悉的两个示例说明如何进行排队。
大多数杂货店都可以增设多个队列。每个收银台都有其自己的队列,因此要购买某些商品,您需要选择一个队列。假如您像我一样,大多数时候选择的队列中都有一个满载而归的顾客排在您前面,他/她购买的很多商品都需要核实价格,而且要使用第三方远期支票进行付款。在轮到您结帐之前,可能在您开始排队时还没有出门的顾客都已经为他们购买的商品付完了款。这个例子说明了使用多个队列时的一个问题:即,假如排在过去不久的文章里的任务需要很长时间才能处理完,后面的任务将无法得到及时处理。
使用多个队列的另一个主要问题是,添加一个队列后需要在各队列之间重新分配任务,在队列之前来回移动任务可能会浪费很多时间。还以上面的杂货店为例,当一个新的收银台开始工作时,想象一下许多顾客推着购物车蜂拥而至的场面吧。
在机场排队虽然航空公司并非高效率的典范,但大多数售票点的工作效率都比杂货店要高,这是因为多个票务代理都在为一个队列服务。由于只有一个队列,因此您不必担心排错了队。假如某个乘客占用了较长的时间,其他票务代理仍可以继续为其他乘客服务(假定有多个票务代理)。
具有多个读取器的一个队列也可以轻松扩展。假如队列太长,可以增加代理而不必打断队列。某些代理也可以在接待完当前乘客后离开,而不会在队列中造成混乱。
尽管可能会偏离主题,但我们仍以机场队列为例来说明基于队列的应用程序的另一个常见问题。假定排在一个队列中的几个人属于一个团队,比如说,我和我的家人正在登记准备去旅行。再假定我的家人由于到达时间不同而分散排在一个队列中。假如我们希望坐在一起,那么代理将需要预留一排坐位。假如我和妻子同时来到不同的票务代理处,则我可能预订了第 4 排的 5 个座位,而我的妻子预订了第 47 排的 5 个座位。这是多读取器队列的一个主要问题:即,假如在不同的线程中同时处理相关的消息,将很难进行协调。请想象一下同时处理订单标题和订单内容的情景。处理订单内容的读取器会认为没有相关的订单标题,因为订单标题还未读入数据库。为了正确工作,订单内容必须多次重新查找订单标题,以确保订单标题只是被延迟而不是不存在。
一种更简单的方法是让第一个到达售票处的乘客将所有相关乘客都叫到前面来,以便由一个售票处为所有这些乘客提供服务。 Service Broker 的功能与此类似,它在接收到一条消息后锁定相关的其他消息。持有锁定的读取器可以接收队列中属于同一组的所有其他消息,而其他读取器都不能读取这些消息。在提交事务之前,消息将一直保持锁定状态。该锁定称为“会话组”锁定。会话组是开发人员定义的相关消息组。例如,处理特定订单所需的所有对话(订单标题、订单内容、库存、发货、帐单等等)可能被放入同一个会话组中。从其中一个会话读取消息时,将锁定会话组中的其他消息,这样只有持有锁定的读取器可以处理队列中的所有相关消息。请注意,只锁定一个组中的消息,这一点非常重要。尽管可以同时处理上百个组,但任一时刻一个线程只处理一个组。在提交或回滚线程创建的事务之前,组将一直处于锁定状态。
我想通过这个例子说明的最后一个问题是,当持有会话组的事务被提交后,该会话组中的另一条消息到达时会发生什么情况。还以售票处为例,当我的家人都登记后,我的一个孩子才到达机场。由于原始事务已经结束,最后一位乘客可以由任一票务代理服务。只有在原始代理留下便条,说明该组中其他人员的位置时,新代理才能知道如何为最后一位乘客安排座位。同样,当处理相关消息组的事务完成时,必须记录会话的“状态”以便当该组中下一条消息到达时,接收该消息的队列读取器了解上一个事务停止时的位置。因为这是数据库应用程序,所以存储该状态的位置应该是一张数据库表。Service Broker 提供了一个会话组 ID,可以使用它来方便地将会话状态和会话中的消息相关联。这是与会话组中每条消息一起显示的唯一标识符。假如唯一标识符用作存储状态的表中的密钥,则消息处理逻辑可以很容易地找到与接收到的每条消息相关联的状态。另外,因为每次只有一个队列读取器可以处理来自特定会话组的消息,所以开发人员不必担心同时有两个事务更新状态行从而导致丢失状态信息。
以上示例说明,多读取器队列是扩展大型应用程序的一种简单而有效的方法。Service Broker 提供的会话组锁定机制使编写使用多读取器队列的应用程序像编写使用单读取器队列的应用程序一样容易。
前面讨论的队列都假定位于一个数据库中。为了开发适用于多种业务情况的松散耦合的分布式数据库应用程序,我们需要进行扩展,以包括分散在网络中(通过可靠的消息传送进行通信)的多个数据库中的队列。我们需要可靠的消息传送,因为假如使用数据库确保队列中消息的完整性,但消息却在传送到其他数据库的过程中丢失,所有工作都将是徒劳的。
Service Broker 使用称为“对话协议”的可靠消息传送协议来确保发送到远程队列的消息按顺序到达并且仅到达一次。正如对话是双向会话一样,对话协议同时支持双向的消息传送。
对话消息具有标题,可确保消息按正确顺序安全地传送到正确的目标位置。它包含序列号、消息所在对话的标识符、要发送到的服务的名称、安全性信息以及用于控制消息传送的一些其他信息。当目标位置成功接收到消息后,它将发出确认,以便源位置知道已成功传送了消息。假如可能,确认信息将包含在另一条消息的标题中发送回源位置,以尽可能减小消息的数量。假如源位置在某一时限内没有收到确认,将重新发送该消息直到成功传送。
消息传送系统在传送较大的消息时经常会遇到问题。发送以 GB 为单位的消息会花几分钟时间,从而会占用一段时间的网络连接。假如发生网络错误导致重新发送消息多次,将会严重影响网络性能。为了解决此问题,Service Broker 对话协议将大型消息拆分成多个较小的片段,然后再单独发送每个片段。假如发生网络错误导致重新发送,则只重新发送传送失败的消息片断。因此,Service Broker 最大可以支持 2GB 的消息,而许多可靠的消息传送系统只能发送 100MB 或更小的消息。
“仅一次”消息处理需要使用事务性消息。为了说明此问题,假定某个应用程序在处理消息的过程中发生了错误。当应用程序重新启动时,它如何才能知道是否要处理发生错误时正在处理的消息呢?数据库中可能已经更新了消息处理的结果,因此重新处理消息可能产生重复的数据。唯一安全的处理方式是使接收消息成为更新数据库的同一事务的一部分。在系统崩溃时数据库更新和消息接收都回滚,因此数据库和消息队列的状态与系统崩溃前的状态相同。
因为所有 Service Broker 操作都在数据库事务环境中发生,所以保证了消息传送操作的 事务完整性。典型的 Service Broker 消息处理事务包括以下步骤:
1. |
开始事务。 |
2. |
从会话组中接收一条或多条消息。 |
3. |
从状态表中检索会话的状态。 |
4. |
处理消息并根据消息内容对应用程序数据进行一项或多项更新。 |
5. |
发送一些 Service Broker 消息:即,将响应发送到传入的消息或将消息发送到处理传入消息所需的其他服务。 |
6. |
假如此会话组包含其他消息,则读取和处理此会话组中的其他消息。 |
7. |
使用新的会话状态更新会话状态表。 |
8. |
提交事务 |
Service Broker 事务性消息传送的一项重要功能是在任何时间假如崩溃或应用程序发生错误,事务将回滚并且一切都返回到事务开始时的状态,即状态不变、应用程序数据不变、消息未发送并且接收的消息返回到队列中。这使此类应用程序中的错误处理非常简单。
在 Service Broker 应用程序的消息处理过程中,队列读取器首先从队列接收消息。由于消息始终从队列中拉出,因此在消息到达队列时接收应用程序必须正在运行。许多异步消息应用程序都面临着一个问题,即如何确保队列读取器在需要时处于运行状态?有两种传统的方法:让队列读取器成为连续运行的服务,或者使用触发器,在每条消息到达时,都将触发消息传送系统。Windows NT 服务方法是指即使在不处理消息时也运行应用程序。由于队列读取器频繁开始和停止,触发器方法可能会遇到性能问题。
Service Broker 采用称为激活的中间状态方法来管理队列读取器。为设置激活,DBA 将存储过程与 Service Broker 队列相关联。当第一条消息到达队列时,激活逻辑将启动指定的存储过程。存储过程负责接收和处理消息,直到队列为空。队列为空后,存储过程可以终止以节省资源。
假如 Service Broker 确定消息添加到队列的速度比存储过程能够处理的速度快,激活逻辑将开始其他的存储过程,直到存储过程能够处理传入的消息或者达到了为队列配置的存储过程的最大数量。由于为队列提供服务的队列读取器数量随着传入消息速率的更改增大或减小,因此所有时间都将运行适当数量的队列读取器。
四、为什么要在数据库中进行消息传送?关于 Service Broker 的一个常见问题是“为什么要将消息传送功能内置在数据库中?数据库外部难道没有足够可靠的消息传送系统?”
我希望前面的信息已经说明了为什么要在数据库引擎中内置 Service Broker,下面提供了要在数据库中传送消息的一些其他理由:
• |
消息和数据的单客户端连接。除了上一部分中提到的统一编程模型,还具有一些其他显著的优势:
| ||||||
• |
数据和消息之间集成的管理、部署和操作。用于保护和管理数据库数据的所有工具和技术同样适用于消息:
| ||||||
• |
将消息传送功能内置在数据库中还有一些显著的性能优势。
|
Service Broker 功能通过 SQL Server 中的新对象启用,这些新对象可以由一组 T-SQL 扩展来创建和操作。为了数据库程序员的方便,使用了他们熟悉的用于配置其他数据库对象的 CREATE、ALTER 和 DROP DDL 语句来配置 Service Broker 应用程序。用于创建 Service Broker 对话以及在对话中发送和接收消息的命令是 Transact SQL 语言的 DML 扩展。接收命令的语法与选择命令相似,它返回包含消息的行集,就像选择命令返回包含行的行集一样。熟悉 Transact SQL 编程的开发人员会发现学习 Service Broker 编程非常容易。用于为 Service Broker 编程的客户端 API 与用于为所有数据库编程的 API 相同,例如 OLEDB、ODBC、ADO、ADO.NET 等等。下面是 Service Broker 使用的对象。
Service Broker 使用队列在消息发送程序和消息接收程序之间提供松散耦合。发送程序可以使用 SEND 命令将消息放到队列中,然后应用程序继续操作并依靠 Service Broker 来确保消息到达其目标位置。
队列允许较大的计划灵活性。例如,发送程序可以发送多条消息以供多个接收程序并行处理。接收程序可能在消息发送很长时间后才处理消息,但由于传入消息进行了排队,因此接收程序可以按其自己的速率处理消息,而且发送程序无须等待接收程序完成处理便可继续操作。
Service Broker 还实现了对话,对话是两个端点之间的双向消息流。对话中的所有消息都进行了排序,而且对话消息总是按照发送的顺序传送。该顺序在事务、输入线程、输出线程以及系统崩溃和重新启动过程中都保持不变。有些消息系统可以确保单个事务中发送或接收的消息顺序,但不能确保多个事务中的顺序,因此 Service Broker 对话具有独特的优势。
对话是一种会话类型。Service Broker 会话是持久可靠的通信通道。在以后的 SQL Server 版本中,Service Broker 将包括一对多的单向会话,也称为可靠的“发布-订阅”。在 SQL Server 2005 中,对话是唯一的会话类型,因此这两个术语同义。
每条消息都包括唯一标识与它相关的对话的会话句柄。例如,某个订单输入应用程序可能同时使用发货应用程序、库存应用程序和帐单应用程序打开了对话。因为每个应用程序中的消息都具有唯一的会话句柄,所以可以轻松地确定发送每条消息的应用程序。
当前,所有 Service Broker 消息都与特定的消息类型相关联。消息类型是与消息一起传送的标签,因此接收消息的应用程序可以确定所接收消息的类型。此外,假如消息包含 XML 文档,则消息类型可以与 XML 架构集合相关联。假如为某个消息类型指定了架构集合,则所接收的该类型消息将在收到时根据架构集合进行验证,而没有通过架构验证的消息将会被拒绝。
Service Broker 规范是一个消息类型集合。一个对话总是与一个规范相关联,而规范定义哪些消息类型可以通过对话发送。
Service Broker 服务将一个或多个规范与一个队列相关联。规范定义可以将哪些消息类型发送到队列。服务名称用于建立对话的端点。服务名称用作实际队列的别名,因此您可以编写引用服务名称的 Service Broker 程序,然后在部署应用程序时将它与实际队列相关联。
五、Service Broker“Hello World”大多数编程语言书籍都以“Hello World”示例开始,因此我们也在 Service Broker 中使用 Hello World 作为示例。为简化示例,我使用可以从 SQL Server Management Studio 查询窗口运行的 T-SQL 进行编写。大多数 Service Broker 应用程序将作为普通的数据库应用程序,这些应用程序由通过 ADO 或 ADO.NET 与数据库进行通信的可执行程序和使用 T-SQL 或 .NET 语言编写的 SQL Server 存储过程构成。
要发送和接收消息,必须首先创建向 Service Broker 描述应用程序的消息传送部分的元数据对象。在 SQL Server 2005 中,使用新添加的 DDL 语句来创建、修改和删除 Service Broker 元数据。
-- 创建要使用的数据库 Create Database HelloWorldDB go Use HelloWorldDB go -- 创建要使用的两种消息类型。大家要使用的消息将是 -- 字符串而不是 XML - 因此无需进行验证 CREATE MESSAGE TYPE [HelloWorldRequest] VALIDATION = NONE CREATE MESSAGE TYPE [HelloWorldResponse] VALIDATION = NONE -- 创建一个限制此对话框中消息类型 -- 的规范。请求由对话框的初始化程序发出 -- 响应消息由对话框目标发送。 CREATE CONTRACT [HelloWorldContract] ( [HelloWorldRequest] SENT BY initiator, [HelloWorldResponse] SENT BY target ) -- 创建对话框在其间通信的两个队列。A -- 对话框请求两个队列。 CREATE QUEUE [HelloWorldTargetQueue] CREATE QUEUE [HelloWorldInitiatorQueue] -- 创建命名对话框端点的服务。服务会将 -- 会话端点连接到队列。 CREATE SERVICE [HelloWorldRequestService] ON QUEUE [HelloWorldTargetQueue] ( [HelloWorldContract] ) CREATE SERVICE [HelloWorldResponseService] ON QUEUE [HelloWorldInitiatorQueue] go
现在已经设置了元数据,可以发送消息了。请注意,由于初始化程序和目标服务位于同一 SQL Server 实例中,因此消息将直接转到目标队列而不会通过传送队列传送。由于 Service Broker 内置在数据库中,因此可以进行此项性能优化。
Use HelloWorldDB go SET NOCOUNT ON DECLARE @conversationHandle uniqueidentifier Begin Transaction -- 开始 Hello World 服务的对话 BEGIN DIALOG @conversationHandle FROM SERVICE [HelloWorldResponseService] TO SERVICE 'HelloWorldRequestService' ON CONTRACT [HelloWorldContract] WITH ENCRYPTION = OFF, LIFETIME = 600; -- 发送消息 SEND ON CONVERSATION @conversationHandle MESSAGE TYPE [HelloWorldRequest] (N'Hello World') commit
让我们查看目标队列以确保成功发送了消息。
Use HelloWorldDB go -- 检查目标队列以确认消息已送达 select * from [HelloWorldTargetQueue] go -- 将消息主体转换为字符串,以便我们查看其中包含的内容 select cast(message_body as nvarchar(MAX)) from [HelloWorldTargetQueue] go
现在可以从目标队列中接收消息并将响应发送回初始化程序。
-- 使用 Receive 命令可从队列接收消息 -- 声明变量以存储接收到的数据 SET NOCOUNT ON DECLARE @conversationHandle uniqueidentifier declare @message_body nvarchar(MAX) declare @message_type_name sysname; -- Service Broker 命令总是位于事务中 Begin Transaction; -- Receive 命令的格式类似于一个选择列表。首先列出 -- 要获取的列,然后指定要从中获取消息 -- 的队列 RECEIVE top(1) -- 只接收一条消息,因此我们可以直接保存到变量中。 @message_type_name=message_type_name, -- 接收的消息类型 @conversationHandle=conversation_handle, -- 对话的标识符 -- 我们通过下列语句接收该消息 @message_body=message_body -- 作为 -- varbinary(MAX) blob 的消息内容 FROM [HelloWorldTargetQueue] print @message_body -- 假如这是一条 hello world 消息,则用相应的问候语回答 if @message_type_name = N'HelloWorldRequest' Begin SEND ON CONVERSATION @conversationHandle -- 使用下列消息接收语句的相同会话 MESSAGE TYPE [HelloWorldResponse] (N'Hello From '+@@servername ) -- 这是我们希望从初始化程序接收的唯一消息,因此 -- 现在可以安全地结束对话。 END CONVERSATION @conversationHandle End -- 提交事务 -- 假如此时我们回滚,所有内容将退回到 -- 我们开始时的状态 – 消息会返回到队列,并且没有发送响应 Commit go -- 确认我们从队列中删除了消息 select cast(message_body as nvarchar(MAX)) from [HelloWorldTargetQueue] go
响应已在对话中发送回初始化程序队列,现在检查响应是否成功到达:
Use HelloWorldDB go select cast(message_body as nvarchar(MAX)) from [HelloWorldInitiatorQueue] go
... 最后接收并显示响应消息:
RECEIVE cast(message_body as nvarchar(MAX)) FROM [HelloWorldInitiatorQueue]
现在已经成功地向 Service Broker 发送了消息并收到了响应。假如希望查看刚刚运行的命令的详细说明,请参见 SQL Server 联机书籍所提供的丰富消息。假如希望扩展此示例以在两个数据库之间使用,您需要添加从发起程序服务器到目标服务器的路由和从目标返回到初始化程序的路由。Service Broker 路由将 BEGIN DIALOG CONVERSATION 命令中指定的服务名称映射到发送消息所需的实际网络地址信息。有关路由和安全性的详细信息,请参见 SQL Server 联机书籍。
六、示例现在您已经看到了 Service Broker 是如何工作的,下面将介绍可以使用 Service Broker 开发的部分应用程序。
许多程序都使用订单输入作为示例,因为大多数人都了解该过程。即使没有开发过订单输入系统的开发人员至少都使用过订单输入系统进行订货。
基于 Service Broker 的订单输入系统使用队列来将系统的子系统连接到一起。这不仅增加了并行度并且提高了效率,而且使配置和体系结构具有非常大的灵活性。
在本示例中,Service Broker 用于连接四个松散耦合的服务以处理每个订单。在订单输入服务将订单标题和订单内容插入到数据库中时,它根据计费、发货、库存和信贷限额服务对消息进行排队以完成对订单的处理。Service Broker 允许四项服务并行运行,从而缩短了系统的响应时间。
根据系统的业务需求,订单输入服务可以等待收到所有四项服务的响应后返回到用户,也可以在提交初始事务后立即返回而在后台执行其余的服务。实现每个操作都会对订单输入服务进行一些更改。每种情况中的其他服务保持不变。
使用 Service Broker 链接服务还提供了几个部署选项。所有五项服务可以在同一台服务器上运行,也可以根据需要分散在五台服务器(在服务的负载平衡时可以为更多的服务器)上运行以提供所需的效率。服务可以部署为存储过程或外部应用程序。这种灵活性对 ISV 非常有用,ISV 可以根据客户对性能、冗余和效率的需求创建单个代码库并在各种配置中进行部署。
图 2:并行处理示例
批处理系统中经常会使用 Service Broker 应用程序。大多数批处理由许多小型的半独立处理构成,必须对这些半独立处理进行计划和协调。独立执行子过程允许每个子系统按其最佳速度执行,因此可以提高效率。
在本示例中,将会不间断地在输入队列上累计对批计划过程的输入,包括预购、预测、返回等等。当计划引擎运行时,它阅读队列中的输入、进行分析、然后将请求排队到处理计划输出的子过程。输出队列允许在一台服务器或多台服务器上并行地单独执行子过程。这允许根据需要扩展任意多台服务器以满足处理负载的需求。
图 4:旅行预订示例
一、引言
SQL Server 2005 的一个主要成就是可以实现可靠、可扩展且功能完善的数据库应用程序。与 .NET Framework 2.0 公共语言运行库 (CLR) 的集成使开发人员可以将重要的业务逻辑与存储过程合并,而 T-SQL 和 XML 中的新增功能扩展了数据操作的可用范围以及开发人员可用的存储功能。另一个重要功能是 SQL Server Service Broker,它为数据库应用程序增加了可靠、可扩展、分布式异步功能。
二、为什么要编写异步排队操作?在开发 SQL Server 2005 时,我们与成功开发过大型可扩展数据库应用程序的人员进行了交谈。结果发现他们几乎所有的应用程序中都有一个或多个操作是以异步排队方式执行的。股票交易系统中的结算活动是排队的,这样可以在后台进行结算,在前端处理其他交易。订单输入系统中的发货信息放在一个排队中,稍后将由另一台服务器(通常位于其他位置)上运行的发货系统读取。旅行预订系统在客户填写完路线后再进行实际的预订,并在预订完成后发送确认电子邮件。在所有这些示例中,许多工作都是通过异步方式完成的,从而提高了系统的响应速度,因此用户无须等待工作完成就可以收到响应。
在大多数大型系统中,经过仔细分析后都可以发现,许多处理都可以通过异步方式完成。虽然应用程序中的排队任务无须立即完成,但系统必须确保这些任务能够可靠地完成。Service Broker 使异步排队任务的执行可靠并且易于实现。
使应用程序中的部分任务异步执行的另一个优势是这些任务的处理工作可以在资源可用时完成。假如订单输入系统中的发货任务可以从队列中执行,发货系统就无需具有处理峰值订单负载的能力。在许多订单输入系统中,峰值订单率可能是平均订单率的两倍或三倍。由于订单输入系统必须具有处理峰值负载的能力,因此大量处理能力在很大一部分时间内都处于闲置状态。假如在出现峰值时对异步任务进行排队并在空闲时执行,将显著提高系统的利用率。
三、为什么排队应用程序难于编写?既然异步排队应用程序具有许多优势,为什么不以这种方式编写所有应用程序?这是因为编写异步排队应用程序相当困难!许多尝试在应用程序中使用数据库表作为队列的开发人员发现,实际操作远比想象的困难。作为队列的表中包含多个进程,包括同时插入、读取和删除几条记录。这将带来并发问题、降低系统性能并导致频繁死机。尽管许多开发人员都成功地解决了这些难题,但实际操作却非常困难。Service Broker 通过使队列成为 SQL Server 2005 数据库中的第一级数据库对象解决了这些问题。编写队列的大多数难题都已解决,因此开发人员可以专注于编写异步应用程序,而不是编写排队基础结构。本文的其余内容将讨论编写排队应用程序涉及到的难题,并解释 Service Broker 如何帮助您解决这些难题。
在许多异步排队应用程序中,排队的消息实际上是有价值的业务对象。例如在订单输入系统中,假如将发货信息放入队列中以便稍后处理,那么丢失排队的数据将意味着无法按订单发货。许多可靠的消息传送系统都将消息存储在文件系统中,因此磁盘驱动器的损坏将导致消息丢失。Service Broker 将消息存储在隐藏的数据库表中,因此 SQL Server 为保护数据而提供的所有数据完整性功能同样可用于保护排队的消息。假如使用数据库镜像进行故障恢复,当因数据库故障而转移到第二个数据库后,所有消息都会转移到第二个数据库中,这样 Service Broker 应用程序将继续运行而不会丢失任何数据。
多读取器队列是一种扩展异步应用程序的最有效的方式。为证明这一点,我将通过大家都熟悉的两个示例说明如何进行排队。
大多数杂货店都可以增设多个队列。每个收银台都有其自己的队列,因此要购买某些商品,您需要选择一个队列。假如您像我一样,大多数时候选择的队列中都有一个满载而归的顾客排在您前面,他/她购买的很多商品都需要核实价格,而且要使用第三方远期支票进行付款。在轮到您结帐之前,可能在您开始排队时还没有出门的顾客都已经为他们购买的商品付完了款。这个例子说明了使用多个队列时的一个问题:即,假如排在过去不久的文章里的任务需要很长时间才能处理完,后面的任务将无法得到及时处理。
使用多个队列的另一个主要问题是,添加一个队列后需要在各队列之间重新分配任务,在队列之前来回移动任务可能会浪费很多时间。还以上面的杂货店为例,当一个新的收银台开始工作时,想象一下许多顾客推着购物车蜂拥而至的场面吧。
在机场排队虽然航空公司并非高效率的典范,但大多数售票点的工作效率都比杂货店要高,这是因为多个票务代理都在为一个队列服务。由于只有一个队列,因此您不必担心排错了队。假如某个乘客占用了较长的时间,其他票务代理仍可以继续为其他乘客服务(假定有多个票务代理)。
具有多个读取器的一个队列也可以轻松扩展。假如队列太长,可以增加代理而不必打断队列。某些代理也可以在接待完当前乘客后离开,而不会在队列中造成混乱。
尽管可能会偏离主题,但我们仍以机场队列为例来说明基于队列的应用程序的另一个常见问题。假定排在一个队列中的几个人属于一个团队,比如说,我和我的家人正在登记准备去旅行。再假定我的家人由于到达时间不同而分散排在一个队列中。假如我们希望坐在一起,那么代理将需要预留一排坐位。假如我和妻子同时来到不同的票务代理处,则我可能预订了第 4 排的 5 个座位,而我的妻子预订了第 47 排的 5 个座位。这是多读取器队列的一个主要问题:即,假如在不同的线程中同时处理相关的消息,将很难进行协调。请想象一下同时处理订单标题和订单内容的情景。处理订单内容的读取器会认为没有相关的订单标题,因为订单标题还未读入数据库。为了正确工作,订单内容必须多次重新查找订单标题,以确保订单标题只是被延迟而不是不存在。
一种更简单的方法是让第一个到达售票处的乘客将所有相关乘客都叫到前面来,以便由一个售票处为所有这些乘客提供服务。 Service Broker 的功能与此类似,它在接收到一条消息后锁定相关的其他消息。持有锁定的读取器可以接收队列中属于同一组的所有其他消息,而其他读取器都不能读取这些消息。在提交事务之前,消息将一直保持锁定状态。该锁定称为“会话组”锁定。会话组是开发人员定义的相关消息组。例如,处理特定订单所需的所有对话(订单标题、订单内容、库存、发货、帐单等等)可能被放入同一个会话组中。从其中一个会话读取消息时,将锁定会话组中的其他消息,这样只有持有锁定的读取器可以处理队列中的所有相关消息。请注意,只锁定一个组中的消息,这一点非常重要。尽管可以同时处理上百个组,但任一时刻一个线程只处理一个组。在提交或回滚线程创建的事务之前,组将一直处于锁定状态。
我想通过这个例子说明的最后一个问题是,当持有会话组的事务被提交后,该会话组中的另一条消息到达时会发生什么情况。还以售票处为例,当我的家人都登记后,我的一个孩子才到达机场。由于原始事务已经结束,最后一位乘客可以由任一票务代理服务。只有在原始代理留下便条,说明该组中其他人员的位置时,新代理才能知道如何为最后一位乘客安排座位。同样,当处理相关消息组的事务完成时,必须记录会话的“状态”以便当该组中下一条消息到达时,接收该消息的队列读取器了解上一个事务停止时的位置。因为这是数据库应用程序,所以存储该状态的位置应该是一张数据库表。Service Broker 提供了一个会话组 ID,可以使用它来方便地将会话状态和会话中的消息相关联。这是与会话组中每条消息一起显示的唯一标识符。假如唯一标识符用作存储状态的表中的密钥,则消息处理逻辑可以很容易地找到与接收到的每条消息相关联的状态。另外,因为每次只有一个队列读取器可以处理来自特定会话组的消息,所以开发人员不必担心同时有两个事务更新状态行从而导致丢失状态信息。
以上示例说明,多读取器队列是扩展大型应用程序的一种简单而有效的方法。Service Broker 提供的会话组锁定机制使编写使用多读取器队列的应用程序像编写使用单读取器队列的应用程序一样容易。
前面讨论的队列都假定位于一个数据库中。为了开发适用于多种业务情况的松散耦合的分布式数据库应用程序,我们需要进行扩展,以包括分散在网络中(通过可靠的消息传送进行通信)的多个数据库中的队列。我们需要可靠的消息传送,因为假如使用数据库确保队列中消息的完整性,但消息却在传送到其他数据库的过程中丢失,所有工作都将是徒劳的。
Service Broker 使用称为“对话协议”的可靠消息传送协议来确保发送到远程队列的消息按顺序到达并且仅到达一次。正如对话是双向会话一样,对话协议同时支持双向的消息传送。
对话消息具有标题,可确保消息按正确顺序安全地传送到正确的目标位置。它包含序列号、消息所在对话的标识符、要发送到的服务的名称、安全性信息以及用于控制消息传送的一些其他信息。当目标位置成功接收到消息后,它将发出确认,以便源位置知道已成功传送了消息。假如可能,确认信息将包含在另一条消息的标题中发送回源位置,以尽可能减小消息的数量。假如源位置在某一时限内没有收到确认,将重新发送该消息直到成功传送。
消息传送系统在传送较大的消息时经常会遇到问题。发送以 GB 为单位的消息会花几分钟时间,从而会占用一段时间的网络连接。假如发生网络错误导致重新发送消息多次,将会严重影响网络性能。为了解决此问题,Service Broker 对话协议将大型消息拆分成多个较小的片段,然后再单独发送每个片段。假如发生网络错误导致重新发送,则只重新发送传送失败的消息片断。因此,Service Broker 最大可以支持 2GB 的消息,而许多可靠的消息传送系统只能发送 100MB 或更小的消息。
“仅一次”消息处理需要使用事务性消息。为了说明此问题,假定某个应用程序在处理消息的过程中发生了错误。当应用程序重新启动时,它如何才能知道是否要处理发生错误时正在处理的消息呢?数据库中可能已经更新了消息处理的结果,因此重新处理消息可能产生重复的数据。唯一安全的处理方式是使接收消息成为更新数据库的同一事务的一部分。在系统崩溃时数据库更新和消息接收都回滚,因此数据库和消息队列的状态与系统崩溃前的状态相同。
因为所有 Service Broker 操作都在数据库事务环境中发生,所以保证了消息传送操作的 事务完整性。典型的 Service Broker 消息处理事务包括以下步骤:
1. |
开始事务。 |
2. |
从会话组中接收一条或多条消息。 |
3. |
从状态表中检索会话的状态。 |
4. |
处理消息并根据消息内容对应用程序数据进行一项或多项更新。 |
5. |
发送一些 Service Broker 消息:即,将响应发送到传入的消息或将消息发送到处理传入消息所需的其他服务。 |
6. |
假如此会话组包含其他消息,则读取和处理此会话组中的其他消息。 |
7. |
使用新的会话状态更新会话状态表。 |
8. |
提交事务 |
Service Broker 事务性消息传送的一项重要功能是在任何时间假如崩溃或应用程序发生错误,事务将回滚并且一切都返回到事务开始时的状态,即状态不变、应用程序数据不变、消息未发送并且接收的消息返回到队列中。这使此类应用程序中的错误处理非常简单。
在 Service Broker 应用程序的消息处理过程中,队列读取器首先从队列接收消息。由于消息始终从队列中拉出,因此在消息到达队列时接收应用程序必须正在运行。许多异步消息应用程序都面临着一个问题,即如何确保队列读取器在需要时处于运行状态?有两种传统的方法:让队列读取器成为连续运行的服务,或者使用触发器,在每条消息到达时,都将触发消息传送系统。Windows NT 服务方法是指即使在不处理消息时也运行应用程序。由于队列读取器频繁开始和停止,触发器方法可能会遇到性能问题。
Service Broker 采用称为激活的中间状态方法来管理队列读取器。为设置激活,DBA 将存储过程与 Service Broker 队列相关联。当第一条消息到达队列时,激活逻辑将启动指定的存储过程。存储过程负责接收和处理消息,直到队列为空。队列为空后,存储过程可以终止以节省资源。
假如 Service Broker 确定消息添加到队列的速度比存储过程能够处理的速度快,激活逻辑将开始其他的存储过程,直到存储过程能够处理传入的消息或者达到了为队列配置的存储过程的最大数量。由于为队列提供服务的队列读取器数量随着传入消息速率的更改增大或减小,因此所有时间都将运行适当数量的队列读取器。
四、为什么要在数据库中进行消息传送?关于 Service Broker 的一个常见问题是“为什么要将消息传送功能内置在数据库中?数据库外部难道没有足够可靠的消息传送系统?”
我希望前面的信息已经说明了为什么要在数据库引擎中内置 Service Broker,下面提供了要在数据库中传送消息的一些其他理由:
• |
消息和数据的单客户端连接。除了上一部分中提到的统一编程模型,还具有一些其他显著的优势:
| ||||||
• |
数据和消息之间集成的管理、部署和操作。用于保护和管理数据库数据的所有工具和技术同样适用于消息:
| ||||||
• |
将消息传送功能内置在数据库中还有一些显著的性能优势。
|
Service Broker 功能通过 SQL Server 中的新对象启用,这些新对象可以由一组 T-SQL 扩展来创建和操作。为了数据库程序员的方便,使用了他们熟悉的用于配置其他数据库对象的 CREATE、ALTER 和 DROP DDL 语句来配置 Service Broker 应用程序。用于创建 Service Broker 对话以及在对话中发送和接收消息的命令是 Transact SQL 语言的 DML 扩展。接收命令的语法与选择命令相似,它返回包含消息的行集,就像选择命令返回包含行的行集一样。熟悉 Transact SQL 编程的开发人员会发现学习 Service Broker 编程非常容易。用于为 Service Broker 编程的客户端 API 与用于为所有数据库编程的 API 相同,例如 OLEDB、ODBC、ADO、ADO.NET 等等。下面是 Service Broker 使用的对象。
Service Broker 使用队列在消息发送程序和消息接收程序之间提供松散耦合。发送程序可以使用 SEND 命令将消息放到队列中,然后应用程序继续操作并依靠 Service Broker 来确保消息到达其目标位置。
队列允许较大的计划灵活性。例如,发送程序可以发送多条消息以供多个接收程序并行处理。接收程序可能在消息发送很长时间后才处理消息,但由于传入消息进行了排队,因此接收程序可以按其自己的速率处理消息,而且发送程序无须等待接收程序完成处理便可继续操作。
Service Broker 还实现了对话,对话是两个端点之间的双向消息流。对话中的所有消息都进行了排序,而且对话消息总是按照发送的顺序传送。该顺序在事务、输入线程、输出线程以及系统崩溃和重新启动过程中都保持不变。有些消息系统可以确保单个事务中发送或接收的消息顺序,但不能确保多个事务中的顺序,因此 Service Broker 对话具有独特的优势。
对话是一种会话类型。Service Broker 会话是持久可靠的通信通道。在以后的 SQL Server 版本中,Service Broker 将包括一对多的单向会话,也称为可靠的“发布-订阅”。在 SQL Server 2005 中,对话是唯一的会话类型,因此这两个术语同义。
每条消息都包括唯一标识与它相关的对话的会话句柄。例如,某个订单输入应用程序可能同时使用发货应用程序、库存应用程序和帐单应用程序打开了对话。因为每个应用程序中的消息都具有唯一的会话句柄,所以可以轻松地确定发送每条消息的应用程序。
当前,所有 Service Broker 消息都与特定的消息类型相关联。消息类型是与消息一起传送的标签,因此接收消息的应用程序可以确定所接收消息的类型。此外,假如消息包含 XML 文档,则消息类型可以与 XML 架构集合相关联。假如为某个消息类型指定了架构集合,则所接收的该类型消息将在收到时根据架构集合进行验证,而没有通过架构验证的消息将会被拒绝。
Service Broker 规范是一个消息类型集合。一个对话总是与一个规范相关联,而规范定义哪些消息类型可以通过对话发送。
Service Broker 服务将一个或多个规范与一个队列相关联。规范定义可以将哪些消息类型发送到队列。服务名称用于建立对话的端点。服务名称用作实际队列的别名,因此您可以编写引用服务名称的 Service Broker 程序,然后在部署应用程序时将它与实际队列相关联。
五、Service Broker“Hello World”大多数编程语言书籍都以“Hello World”示例开始,因此我们也在 Service Broker 中使用 Hello World 作为示例。为简化示例,我使用可以从 SQL Server Management Studio 查询窗口运行的 T-SQL 进行编写。大多数 Service Broker 应用程序将作为普通的数据库应用程序,这些应用程序由通过 ADO 或 ADO.NET 与数据库进行通信的可执行程序和使用 T-SQL 或 .NET 语言编写的 SQL Server 存储过程构成。
要发送和接收消息,必须首先创建向 Service Broker 描述应用程序的消息传送部分的元数据对象。在 SQL Server 2005 中,使用新添加的 DDL 语句来创建、修改和删除 Service Broker 元数据。
-- 创建要使用的数据库 Create Database HelloWorldDB go Use HelloWorldDB go -- 创建要使用的两种消息类型。大家要使用的消息将是 -- 字符串而不是 XML - 因此无需进行验证 CREATE MESSAGE TYPE [HelloWorldRequest] VALIDATION = NONE CREATE MESSAGE TYPE [HelloWorldResponse] VALIDATION = NONE -- 创建一个限制此对话框中消息类型 -- 的规范。请求由对话框的初始化程序发出 -- 响应消息由对话框目标发送。 CREATE CONTRACT [HelloWorldContract] ( [HelloWorldRequest] SENT BY initiator, [HelloWorldResponse] SENT BY target ) -- 创建对话框在其间通信的两个队列。A -- 对话框请求两个队列。 CREATE QUEUE [HelloWorldTargetQueue] CREATE QUEUE [HelloWorldInitiatorQueue] -- 创建命名对话框端点的服务。服务会将 -- 会话端点连接到队列。 CREATE SERVICE [HelloWorldRequestService] ON QUEUE [HelloWorldTargetQueue] ( [HelloWorldContract] ) CREATE SERVICE [HelloWorldResponseService] ON QUEUE [HelloWorldInitiatorQueue] go
现在已经设置了元数据,可以发送消息了。请注意,由于初始化程序和目标服务位于同一 SQL Server 实例中,因此消息将直接转到目标队列而不会通过传送队列传送。由于 Service Broker 内置在数据库中,因此可以进行此项性能优化。
Use HelloWorldDB go SET NOCOUNT ON DECLARE @conversationHandle uniqueidentifier Begin Transaction -- 开始 Hello World 服务的对话 BEGIN DIALOG @conversationHandle FROM SERVICE [HelloWorldResponseService] TO SERVICE 'HelloWorldRequestService' ON CONTRACT [HelloWorldContract] WITH ENCRYPTION = OFF, LIFETIME = 600; -- 发送消息 SEND ON CONVERSATION @conversationHandle MESSAGE TYPE [HelloWorldRequest] (N'Hello World') commit
让我们查看目标队列以确保成功发送了消息。
Use HelloWorldDB go -- 检查目标队列以确认消息已送达 select * from [HelloWorldTargetQueue] go -- 将消息主体转换为字符串,以便我们查看其中包含的内容 select cast(message_body as nvarchar(MAX)) from [HelloWorldTargetQueue] go
现在可以从目标队列中接收消息并将响应发送回初始化程序。
-- 使用 Receive 命令可从队列接收消息 -- 声明变量以存储接收到的数据 SET NOCOUNT ON DECLARE @conversationHandle uniqueidentifier declare @message_body nvarchar(MAX) declare @message_type_name sysname; -- Service Broker 命令总是位于事务中 Begin Transaction; -- Receive 命令的格式类似于一个选择列表。首先列出 -- 要获取的列,然后指定要从中获取消息 -- 的队列 RECEIVE top(1) -- 只接收一条消息,因此我们可以直接保存到变量中。 @message_type_name=message_type_name, -- 接收的消息类型 @conversationHandle=conversation_handle, -- 对话的标识符 -- 我们通过下列语句接收该消息 @message_body=message_body -- 作为 -- varbinary(MAX) blob 的消息内容 FROM [HelloWorldTargetQueue] print @message_body -- 假如这是一条 hello world 消息,则用相应的问候语回答 if @message_type_name = N'HelloWorldRequest' Begin SEND ON CONVERSATION @conversationHandle -- 使用下列消息接收语句的相同会话 MESSAGE TYPE [HelloWorldResponse] (N'Hello From '+@@servername ) -- 这是我们希望从初始化程序接收的唯一消息,因此 -- 现在可以安全地结束对话。 END CONVERSATION @conversationHandle End -- 提交事务 -- 假如此时我们回滚,所有内容将退回到 -- 我们开始时的状态 – 消息会返回到队列,并且没有发送响应 Commit go -- 确认我们从队列中删除了消息 select cast(message_body as nvarchar(MAX)) from [HelloWorldTargetQueue] go
响应已在对话中发送回初始化程序队列,现在检查响应是否成功到达:
Use HelloWorldDB go select cast(message_body as nvarchar(MAX)) from [HelloWorldInitiatorQueue] go
... 最后接收并显示响应消息:
RECEIVE cast(message_body as nvarchar(MAX)) FROM [HelloWorldInitiatorQueue]
现在已经成功地向 Service Broker 发送了消息并收到了响应。假如希望查看刚刚运行的命令的详细说明,请参见 SQL Server 联机书籍所提供的丰富消息。假如希望扩展此示例以在两个数据库之间使用,您需要添加从发起程序服务器到目标服务器的路由和从目标返回到初始化程序的路由。Service Broker 路由将 BEGIN DIALOG CONVERSATION 命令中指定的服务名称映射到发送消息所需的实际网络地址信息。有关路由和安全性的详细信息,请参见 SQL Server 联机书籍。
六、示例现在您已经看到了 Service Broker 是如何工作的,下面将介绍可以使用 Service Broker 开发的部分应用程序。
许多程序都使用订单输入作为示例,因为大多数人都了解该过程。即使没有开发过订单输入系统的开发人员至少都使用过订单输入系统进行订货。
基于 Service Broker 的订单输入系统使用队列来将系统的子系统连接到一起。这不仅增加了并行度并且提高了效率,而且使配置和体系结构具有非常大的灵活性。
在本示例中,Service Broker 用于连接四个松散耦合的服务以处理每个订单。在订单输入服务将订单标题和订单内容插入到数据库中时,它根据计费、发货、库存和信贷限额服务对消息进行排队以完成对订单的处理。Service Broker 允许四项服务并行运行,从而缩短了系统的响应时间。
根据系统的业务需求,订单输入服务可以等待收到所有四项服务的响应后返回到用户,也可以在提交初始事务后立即返回而在后台执行其余的服务。实现每个操作都会对订单输入服务进行一些更改。每种情况中的其他服务保持不变。
使用 Service Broker 链接服务还提供了几个部署选项。所有五项服务可以在同一台服务器上运行,也可以根据需要分散在五台服务器(在服务的负载平衡时可以为更多的服务器)上运行以提供所需的效率。服务可以部署为存储过程或外部应用程序。这种灵活性对 ISV 非常有用,ISV 可以根据客户对性能、冗余和效率的需求创建单个代码库并在各种配置中进行部署。
图 2:并行处理示例
批处理系统中经常会使用 Service Broker 应用程序。大多数批处理由许多小型的半独立处理构成,必须对这些半独立处理进行计划和协调。独立执行子过程允许每个子系统按其最佳速度执行,因此可以提高效率。
在本示例中,将会不间断地在输入队列上累计对批计划过程的输入,包括预购、预测、返回等等。当计划引擎运行时,它阅读队列中的输入、进行分析、然后将请求排队到处理计划输出的子过程。输出队列允许在一台服务器或多台服务器上并行地单独执行子过程。这允许根据需要扩展任意多台服务器以满足处理负载的需求。
图 4:旅行预订示例