概述
BizTalk Server 业务流程引擎可管理复杂流程和/或事务集的状态,对其应用业务逻辑,并调用其支持应用程序。
业务流程可由使用原子事务的若干分立工作组成,这些原子事务在发生错误或长时间运行时自动回滚所有更改,业务流程可包含嵌套事务,并可使用自定义异常处理从错误中恢复。这些事务性语义通常由业务流程设计器中的“作用域”构造管理。
长期流程可持续数天、数周,甚至更长的时间。长期流程通常利用相关将收到的消息和要发送的消息关联起来。业务流程引擎通常会冻结这些实例以节省系统资源,并在收到相关消息后解除冻结。业务流程引擎会在已知检查点处将业务流程状态持久化到 MessageBox 数据库,以便为从任何应用程序异常或系统异常中恢复做好准备。
提供给 BizTalk 业务流程引擎的事务性编程模型不仅支持异常处理,还支持从失败的事务、在错误发生时自动回滚其操作的原子事务或包含其他事务的长期事务及自定义异常处理中恢复。
原子事务
BizTalk 业务流程可设计为按照事务的传统“ACID”概念,执行不同部分的工作。在执行时,这些离散的或原子的工作单位会将业务流程从一个一致的状态转移到独立于其他工作单位的一致且持久的新状态。这通常是使用“作用域”构造实现的,作用域构造使用事务性语义封装这些工作单位。也可以将整个业务流程定义为一个原子事务,而不使用作用域。但是,这些作用域不能标记为事务性的,除非业务流程本身标记为长期或原子事务类型。原子事务保证在事务性更新期间发生故障时可自动回滚任何部分更新,并且消除事务的影响(事务中进行的任何 .NET 调用的影响除外)。BizTalk 业务流程中的原子事务与分布式事务处理协调器 (DTC) 事务大体相似,只是前者通常存活时间较短并且具有四个“ACID”属性(原子性、一致性、隔离性和持久性):
- 原子性
一个事务就表示一个原子的工作单位。要么在事务内执行所有修改,要么不执行任何修改。 - 一致性
在提交时,事务必须在系统内保持数据的整体性。如果某一事务在该事务开始前对已在内部保持了一致的数据库执行数据修改,则在提交该事务时,数据库仍必须在内部保持一致。确保此属性的工作主要由应用程序开发人员负责。 - 隔离性
并行事务进行的修改必须与其他并行事务进行的修改隔离。并行运行的隔离事务所执行的修改将保持内部数据库一致性,与顺序运行事务完全一样。 - 持久性
在提交某一事务后,默认情况下所有修改都永久存在于系统中。即使系统发生故障,这些修改也存在。
在 BizTalk 业务流程中使用的原子事务支持以下隔离级别:
- 提交读
在正在读取数据时保持共享锁,以避免脏读,但是在事务结束之前可以更改数据,从而导致不可重复的读取或幻像数据。
- 可重复读
在查询中使用的所有数据上放置锁,以防止其他用户更新这些数据。这防止了不可重复的读取,但仍有可能产生幻像行。 - 可序列化
放置一个范围锁,防止在事务完成前其他用户更新行或将行插入数据库中。
BizTalk Server 确保原子事务内的状态变化(例如对变量、消息和对象的修改)只在事务提交后在原子事务的作用域外可见。中间状态的变化将与业务流程的其他部分隔离开来。
如果要求对数据具有完全的 ACID 属性(例如,数据必须独立于其他事务),则必须只使用原子事务。
在原子事务失败时,所有状态都将重置,就像业务流程实例从来没有进入该作用域。BizTalk 针对原子事务的规则是:所有变量(而不只是作用域的本地变量)都参与该事务。在原子事务中使用的所有非序列化变量和消息都应声明为对作用域而言是本地的;否则,编译器将显示“变量…未标记为可序列化”错误。所有原子作用域都将认为是“同步的”,并且如果实际上将同步的关键字用于原子作用域,则业务流程编译器将对冗余的使用发出警告。共享数据的同步将从作用域开始一直扩展到作用域成功完成(包括在作用域结束时的状态持久化),或者扩展到异常处理程序完成时(如果出现错误)。同步域并不扩展到补偿处理程序。
原子事务可与超时值(此时,业务流程将停止事务并且实例将挂起)相关联。如果某一原子事务包含接收形状、发送形状或启动业务流程形状,则相应操作将不会在提交该事务前发生。
如果用户故意引发 RetryTransactionException,或者在原子事务尝试提交时引发 PersistenceException,原子事务将重试。例如,如果原子事务是分布式 DTC 事务的一部分,而该事务中的其他参与者停止了该事务,则可能会引发 PersistenceException。同样,如果在事务正尝试提交时存在数据库连接问题,则也可能引发 PersistenceException。如果 Retry=True 的原子作用域发生此情况,则它将重试最多 21 次。默认情况下,每次重试之间的延迟为 2 秒(但可以修改该默认值)。在执行完全部 21 次重试后,如果该事务仍无法提交,则挂起整个业务流程实例。该实例可以手动恢复,并且将从冲突的原子作用域的开始处使用新计数器重新开始。
DTC 事务
尽管原子事务在行为上类似 DTC 事务,但默认情况下,它们并不是显式的 DTC 事务。可以显式地使这些事务成为 DTC 事务,只要要在该事务中使用的任何对象都是从 System.EnterpriseServices.ServicedComponents 派生的 COM+ 对象,并且隔离级别在事务组件之间达成一致。
非序列化对象
如果想要在某一业务流程内使用非序列化对象,则必须只在原子事务内使用它。只要业务流程由引擎保存,在原子事务外使用此类对象都可能会导致数据丢失。
使用原子事务的方案
方案 1:具有 COM+ ServicedComponent 的原子事务
下面的业务流程显示如何将 RetryTransactionException 与原子事务一起使用。尽管无法为原子作用域直接包括异常处理程序,但该作用域可以包括可具有异常处理程序的非事务性作用域。ServicedComponent 在同一 DTC 事务中登记,捕获由该组件引发的任何异常并重新引发为 RetryTransactionException。(假定针对原子作用域将 Retry 属性设置为 True)。
请注意,即使未引发 RetryTransactionException,仍将挂起此业务流程,并将回滚 MessageAssignment 形状中的操作。但是,这种模式能够在自动进行重试的应用程序中提供一定的弹性。
具有COM+ ServicedComponent 的原子事务
方案 2:将事务性适配器与原子事务一起使用
下面的业务流程显示如何将原子事务与 SQL 适配器一起使用。整个业务流程被标记为长期业务流程,并且带有分别用于工作的两个逻辑部分的单独原子事务:插入新客户和插入客户订单详细信息。
如果由于某种原因导致插入订单的操作失败,则应回滚插入客户的操作。该示例使用 SQL 适配器执行数据库工作。如前所述,消息发送到 MessageBox 数据库时将完成与原子事务相关联的作用域。这表示引擎在 Scope_InsertCustomer 和 Scope_InsertOrder 作用域中成功发送消息后,将提交每一作用域。SQL 适配器将为客户或订单的实际插入创建一个新事务。
这些端口具有“送达通知”属性,用来验证是否通过发送端口成功发送了消息。将“送达通知”属性设置为“已传输”时,会在发送操作的事务性提交点之前放置一个接收订阅。但是在原子作用域的情况下,会将接收订阅放在封闭的父作用域中。
在 InsertOrder SQL 事务失败的情况下,将发送回“Nack”并提交“Scope_InsertOrder”。发送端口用尽所配置的重试次数后,将引发DeliveryFailureException。将运行默认补偿过程的默认异常处理程序会捕获此异常。这将调用与Scope_InsertCustomer 和 Scope_InsertOrder 相关联的补偿处理程序,从而导致撤销插入客户信息的操作。
具有原子事务的事务性适配器
长期事务
长期事务是 BizTalk 业务流程中常用的重要构造。它们提供的功能可帮助实现自定义的基于作用域的补偿、自定义的基于作用域的异常处理以及嵌套事务,因此,可以很灵活地设计可靠的事务结构。
如果事务可能需要长时间运行并且不需要完整的 ACID 属性(即,不需要确保数据与其他事务隔离),则可以使用长期事务。长期事务可能具有较长的不活动时间,通常是由于等待外部消息到达造成的。
长期事务拥有一致性和持久性,但不具备原子性和隔离。长期事务内的数据不锁定;其他进程或应用程序可以对数据进行修改。不保持状态更新的隔离属性,因为保持长期锁定不现实。
长期事务的提交不同于原子事务的提交。没有针对结果的分布式协调的隐式假定(长期事务仅存在于单个业务流程实例中)。而是在长期事务中的最后一个语句完成时,代表该事务已提交。事务中止的情况下,不会“自动”回滚状态。这种回滚可以通过编写异常处理程序和补偿处理程序来实现。
作用域可通过声明变量、消息和 .NET 组件,定义自己的状态。长期事务能够访问自己的作用域的状态信息、封闭它的任何作用域以及在业务流程内全局定义的任何状态信息。长期事务不能够访问不封闭它的任何作用域的状态信息。
嵌套
长期事务可以包含原子事务或其他长期事务。它们可以嵌套到任意深度。例如,的事务可以包含两个其他的长期事务,这两个事务又可以包含原子事务。
如果整体事务的一个或多个组件需要是原子的,而该整体事务又需要是长期的,则嵌套将很有用。让我们看一下一个接收和履行采购订单的例子。该采购订单可能会在任何时候到达,并且履行订单的不同步骤可能需要一定时间才能完成,但仍想要将整个过程作为一个事务处理。显然,此例子中的整体事务需要是长期事务,但对于个别步骤,例如确认付款,可能需要是原子的。
补偿
长期事务可以指定某一补偿模块,此补偿模块将在事务提交后被调用以补偿该事务的活动。它可能只是撤消事务(如果可能),或者执行以某种方式帮助减轻事务影响的某些其他功能(例如通知)。如果没有添加自己的补偿代码,则默认情况下运行时引擎将调用内部事务的补偿模块,长期事务和原子事务都采用相反顺序,即以最后提交的事务开始、以最先提交的事务结束。
容错
事务支持容错,以便从内部错误(例如机器故障和软件错误)和外部错误(例如取消消息)恢复。在发生事务错误时,并不自动回滚长期事务内的部分更新,因为它们位于 ACID 事务中。
在发生错误时将调用长期事务的异常代码块。异常代码块包含一组编写的错误处理程序,用于处理在事务执行期间可能导致的任何错误。可以借助消息、变量和对象的最后已知状态来处理错误。
通常,将希望异常处理程序评估在发生异常时业务流程的状态,基于该状态采取任何必要措施,并且调用任何嵌套事务的补偿。
在发生错误时,将中断长期事务的执行。长期事务不能在发生错误后恢复。
使用长期事务的方案
方案 1:将长期事务用于超时
长期作用域可与某一超时关联,超时是长期工作必须在此时间范围内完成的逻辑时间。如果该作用域没有在指定时间内完成,将引发预定义的系统异常 TimeoutException。
您可以通过将整个业务流程标记为长期的,或者通过让外部长期作用域嵌套任何其他作用域,创建长期进程。在前一个方案中,系统提供的异常处理程序将运行,而后一个方案则允许特定的异常处理程序与外部作用域相关联。默认的系统提供的异常处理程序将为每个成功完成的嵌套事务性作用域(如果有)运行补偿处理程序,运行顺序与其完成顺序相反。您可以通过在异常处理程序中将补偿形状用于长期事务,通过自补偿实现同一任务。
下面的业务流程介绍如何将超时与长期事务相关联。
具有超时值的长期事务
有时候,您可能需要连接以批处理形式操作的旧式系统。此方案显示了一个要接收并发送到旧式系统的采购订单。此旧式系统处理该采购订单,并且发送回采购订单确认。该发送操作采用采购订单号初始化某一相关集,并且接收操作将遵循该相关集。该接收操作还处于具有超时值的长期作用域中。
业务流程引擎将冻结正等待接收的业务流程实例。相关将确保在接收消息后调用同一业务流程实例。如果该采购订单确认在超时值指定的时间间隔内未到达,将引发 TimeoutException。
方案 2:将长期事务用于自定义补偿
以下业务流程阐释如何关联自定义补偿以及调用与整个业务流程相关联的自定义补偿。此方案将插入一个新客户以及插入该客户的订单明细。业务流程的逻辑规定如果订单插入失败,您应该回滚该客户插入。该客户插入可由旧式系统执行,因此,在单独的可调用业务流程中阐释。被调用的业务流程具有为补偿设置的 Custom 属性,这将提供单独的表格来执行补偿过程。该补偿是要删除新插入的客户。
调用业务流程具有要执行订单插入的长期作用域。此作用域嵌套在某一外部长期作用域中。该外部作用域具有关联的异常处理程序,以便捕获任何异常。该处理程序使用补偿形状来定义与调用的业务流程相关联的自定义异常,以便回滚在对业务流程的调用中可能发生的任何更改。
具有自定义补偿的长期事务
调用的业务流程(主要)
调用的业务流程(补偿)
业务流程中的持久化
业务流程引擎将保存业务流程实例在不同持久化点时的整个状态,以便可以解除冻结业务流程实例。该状态包括可在业务流程中使用的所有基于 .NET 的组件以及消息和变量。该引擎存储处于以下持久化点时的状态:
- 事务作用域(原子事务或长期事务)结束时
- 在调试断点时
- 在通过启动业务流程形状执行其他业务流程时
- 在发送形状上(原子事务中除外)
- 在挂起业务流程实例时
- 在系统以可控方式关闭时
- 在引擎确定它要解除冻结时
- 在业务流程实例完成时
引擎在持久化点占用高昂系统资源(尤其是在处理大消息)时优化持久化点的数目。如下面的两个业务流程实例所示,在原子作用域中含发送形状的业务流程中,引擎确定事务作用域结束和业务流程结束之间的单个持久化点。在其他业务流程中,存在两个持久化点,一个用于第一个发送形状,第二个用于发送形状外加业务流程结束。
业务流程持久化
在业务流程中使用的任何基于 .NET 的对象(无论是直接的还是间接的)都必须标记为可序列化,但以下情况除外:对象在原子作用域中被调用,或者对象是无状态的并且只通过静态方法调用。System.Xml.XmlDocument 是特例,它不需要标记为可序列化(与作用域的事务属性无关)。
针对 System.Xml.XmlDocument 的特殊处理的工作原理:
在用户定义 T 类型的变量 X 时,其中,T 是 System.Xml.XmlDocument 或从 System.Xml.XmlDocument 派生的类,编译器会将 X 视为可序列化对象。
在序列化 X 时,运行时将保留以下信息片断:(a) 对象 X 所引用的实际类型 Tr (b) 文档的 OuterXml 字符串。
在对 X 执行反序列化时,运行时将创建 Tr 的一个实例(假定是不取任何参数的构造函数),并且将调用 LoadXml,向该实例提供保存的 OuterXml。X 将设置为指向新创建的 Tr 实例。