SQL Server 2005 Service Broker
一、引言
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“Hello World” 六、示例 批处理
批处理系统中经常会使用 Service Broker 应用程序。大多数批处理由许多小型的半独立处理构成,必须对这些半独立处理进行计划和协调。独立执行子过程允许每个子系统按其最佳速度执行,因此可以提高效率。
在本示例中,将会不间断地在输入队列上累计对批计划过程的输入,包括预购、预测、返回等等。当计划引擎运行时,它阅读队列中的输入、进行分析、然后将请求排队到处理计划输出的子过程。输出队列允许在一台服务器或多台服务器上并行地单独执行子过程。这允许根据需要扩展任意多台服务器以满足处理负载的需求。
旅行预订
以前,我经常为硕士研究生讲授分布式数据库类。我曾经使用的一个分布式事务的示例是旅行社在同一事务中预订了飞机票、旅馆房间和租车服务,因为客户在无法进行航空旅行时不会预订旅馆。像许多示例一样,直到有一天一位来自真正旅行社的程序员来到了教室里,我们才了解真实的情况。他告诉我真正的旅行社不是这样工作的。在预订旅馆房间时,航空公司不允许任何人锁定其预订表格和座位安排。预订根据当前的可用性完成,并且如果在实施预订时已经没有空余的座位或房间,将给客户打电话以确保新的预订满足客户需求。
在旅行预订 Web 站点示例中,预订根据有关航空公司和旅店可用性的数据表来完成。这些表包含来自航空公司或旅馆的信息。信息会经常更新,但总是会有一点过时。实际的预订在客户预约旅行后才发生,并可能在客户取消旅行后被取消。
这种延迟的活动非常适合 Service Broker 体系结构。记录客户旅行安排的事务将消息提交到进行实际预订的后端服务。预订服务读取队列中的消息并在单独的事务中处理每个预订。预订服务通过各种协议与预订航空公司和旅馆服务的系统进行通信。这些通信可能包括 Web 服务、SNA、HTTP、EDI、传真、Service Broker 等等。由于对到预订服务的输入进行了排队,因此这些协议的不同滞后时间不会带来问题。如果队列太长,Service Broker 激活可以启动更多的队列读取器来处理负载。如果负载达到预订服务器能够处理的最大值,则可以通过向路由表添加行来添加更多的服务器。
在成功完成特定路线的所有预订后,会将一条消息排队到 SQLiMail 服务器以向客户发送确认电子邮件。如果一项或多项预订未能完成,将通知客户服务代表来帮助重新预订旅行。
“处理更新”服务在后台运行,并定期从旅馆和航空公司接收可用性信息。这些信息被转换为常见的 XML 格式,然后通过通知服务发布到 Web 集群中的服务器以更新旅馆和航空公司的可用性表。
松散耦合的 Service Broker 体系结构允许通过加载软件、配置数据库和订阅可用性反馈来将 Web 服务器添加到服务器集群。可以编写脚本以使进行操作时只需最少的手动干预。为向预订服务添加更多后端服务器,必须安装和配置该服务,然后必须将新的服务器地址添加到 Web 服务器中的路由表。