Biztalk中有个Convoys概念,翻译成中文叫保护。用在当您需要把一组具有你设定的同类属性的消息交给同一个orchestration实例进行处理时的场景。当具有这样的属性的第一个消息到达biztalk后,biztalk激活一个orchestration实例处理此消息,处理完这个消息后,orchestration实例并不退出而是继续等待后续具有相同属性的消息,这个之后具有跟第一个消息同样属性的消息都被路由到这个orchestration实例,直到在orchestration内根据一定的条件由流程自行结束这个orchestration实例。
Convoys分两种:Sequential Convoys和Parallel Convoys。本文先讨论Sequential Convoys
顺序保护有以下特点:
l 每个顺序保护必须有一个相关集,所有被保护的消息都跟这个相关集有关,这个相关集叫做Convoys set
l 由一个激活接收形状启动流程,并在接受形状里初始化Convoys set,后续的消息在后面的非激活接受形状根据这个初始化后的Convoys set接收相符的消息。
l 顺序保护处理的接收的端口必须与初始化保护集的接收的端口相同。不支持跨端口保护。
l 需要保护处理的接收的消息类型必须与初始化保护集的接收的消息类型匹配,除非接收语句所操作的是按序送达端口。
l 一个Convoys中有几个接收形状就会产生几个类似的订阅,不过里面只应该有一个是fCanActivateConvoySet字段为1,表示是Convoys激活订阅。其它的订阅都是Convoys实例订阅。
下面的图是顺序保护的两种形式:
并行保护有以下特点:
l 每个并行保护必须有一个相关集,所有被保护的消息都跟这个相关集有关,这个相关集叫做Convoys set
l 所有并行保护的接受形状都是激活接收形状,并在所有接受形状中初始化Convoys set
l 各个接收形状的端口可以不同。
设计一个简单的测试场景--订单接收。
一个orchestration接收订单,但是一个订单不是以一个完整的消息形式发送过来,被分成两种消息。
第一种是OrderHead类型的消息,表示订单头,是关于整个订单的消息,其中包括OrderID字段和一个表示这个订单里面包含了多少个具体的Item项数的字段ItemsCount。
第二种OrderItem类型的消息,就是订单的具体Item项,包括这个Item项所属的订单的OrderID,然后是描述每个订单项的item代码,数量等等。
一个完整的订单有一个OrderHead类型的消息和多个OrderItem类型的消息组成。这里的消息设计只是为了测试目的,内容十分简单,能说明问题即可。
需要设计一个orchestration达到以下的目标:
l 同一个订单号的所有消息,即OrderID一样的消息需要在一个orchestration服务实例中运行。
l 每一个订单的OrderHead消息激活一个orchestration实例,不管别的OrderID订单的orchestration实例是否还在运行,后续的OrderItem消息都在这个orchestration实例处理。
Orchestration流程如下图:
属性架构PropertySchemOrderID.xsd很简单,就一个属性OrderID,此属性用来设计相关型和相关集。
设计相关型CorrelationType_SequentialConvoys,其中只包含一个属性,就是前面那个属性架构中设计的属性OrderID。
新建一个相关集Correlation_SequentialConvoys,相关型为CorrelationType_SequentialConvoys。
这个相关集是用来给Sequential Convoys做Convoys set的。
前面测试场景中已经说过订单的两种类型的消息,这两个消息里都有OrderID元素,分别升级这个OrderID元素对应到属性架构PropertySchemOrderID的OrderID。
在项目中建两个消息MsgOrderHead和MsgOrderItem分别对这两个消息类型。
接收形状Receive_Head设置为:OrderHead消息类型,激活、初始化相关集Correlation_SequentialConvoy。
接收形状Receive_Item设置为:OrderItem消息类型,非激活,跟随相关集Correlation_SequentialConvoys。
由于是测试Convoys,对进入到orchestration的消息不做具体的处理,都只是在Expression中把相关信息写入到日志,以确定消息接收的是否正确。
在实际应用中,完全可以在一个orchestration实例中把一个订单的所有消息都收齐后,形成一个完整的订单消息,最后一次性的输出到一个端口。
内容如下:
System.Diagnostics.EventLog.WriteEntry("SequentialConvoysTest -- OrderHead","SequentialConvoys");
CountOrderItem = MsgOrderHead.ItemsCount;
MsgOrderHead消息中的ItemsCount表示这个订单一共有多少个Item项组成,也即这个OrderHead消息后续还要有多少个订单项的消息。
Orchestration中的循环条件是:
CountOrderItem > 0
表示,当订单项数还没到MsgOrderHead.ItemsCount指定的数量时继续循环。
内容如下:
CountOrderItem = CountOrderItem - 1;
System.Diagnostics.EventLog.WriteEntry("SequentialConvoysTest -- OrderItem:" + MsgOrderItem.ItemID,"SequentialConvoys");
收到一个订单项,就把这个订单项的ItemID输出,表示收到了这个订单项。
内容如下:
System.Diagnostics.EventLog.WriteEntry("SequentialConvoysTest -- End","SequentialConvoys");
流程的循环条件不满足后,结束循环,整个订单处理结束,退出流程。
看这个Orchestration,按照一般的消息订阅的经验,Orchestration在Enlist后,应该产生一个激活订阅,就是接收形状Receive_Head连接的Port_Order订阅的OrderHead类型的消息。
另外一个Orchestration实例被激活运行后,接收形状Receive_Head初始化相关集Correlation_SequentialConvoys后,在跟随这个相关集的非激活形状Receive_Item会产生一个订阅OrderItem类型消息的实例订阅。
看一下这个项目在部署运行后生成的订阅关系:
这个项目生成了两个订阅,而且都是激活订阅,这个图里面的Name和Service Name字段由于太长没有全部显示出来,下面完整的列出这两个字段的完整内容:
第一个订阅(后面简称[1]订阅):
Name:Activate: SequentialConvoysTest.SequentialConvoysOrche(9e2c3ea7-7a5c-d8b1-0292-808c42a7f046)[1]
Service Name:SequentialConvoysTest.SequentialConvoysOrche, SequentialConvoysTest, Version=1.0.0.0, Culture=neutral, PublicKeyToken=380d269ec565a18c
第二个订阅(后面简称[0]订阅):
Name:Activate: SequentialConvoysTest.SequentialConvoysOrche(9e2c3ea7-7a5c-d8b1-0292-808c42a7f046)[0]
Service Name:SequentialConvoysTest.SequentialConvoysOrche, SequentialConvoysTest, Version=1.0.0.0, Culture=neutral, PublicKeyToken=380d269ec565a18c
可以看出,实际上SequentialConvoysOrche产生了两个激活订阅,跟我们前面想像的不一样。实际上,Convoys的订阅跟一般的订阅的机制不同,他有单独的一套订阅和路由机制,下面对Convoys的一套订阅路由机制做详细的分析。
分析订阅关系,先看一下biztalk的订阅表Subscription的主要内容:
字段
|
数据类型
|
含义
|
nvcName
|
nvarchar
|
订阅名称
|
uidSubID
|
uniqueidentifier
|
此订阅的uid
|
uidClassID
|
uniqueidentifier
|
产生订阅的服务类型,是adm_ServiceClass 服务表UniqueId字段的外键
|
uidServiceID
|
uniqueidentifier
|
产生此订阅的具体服务
|
uidInstanceID
|
uniqueidentifier
|
实例订阅的情况,此字段是产生此订阅的服务实例的Guid
|
uidPredicateGroupID
|
uniqueidentifier
|
订阅谓词组id
|
fEnabled
|
smallint
|
订阅状态: |
uidConvoySetID
|
uniqueidentifier
|
如果是Convoys订阅,这个字段就是这个Convoys订阅相关的ConvoySetID
|
fCanActivateConvoySet
|
Int
|
如果是Convoys订阅,这个订阅是否可以激活订阅
|
注意最后两个字段uidConvoySetID,fCanActivateConvoySet。
一个Convoys必然跟相关集有关系,这个相关集对于Convoys就叫做Convoys set。一般的由第一个激活接收形状激活一个Orchestration实例,并初始化这个Convoys set的一个实例,后续的消息就是根据这个初始化后的Convoys set实例的值是否匹配而被路由到相关的Orchestration实例。
Biztalk的数据库中有张Convoys set的表,表名是ConvoySets,结构如下:
字段
|
数据类型
|
含义
|
uidConvoySetID
|
uniqueidentifier
|
ConvoySetID
|
nvcConvoySetName
|
nvarchar
|
名称
|
uidServiceID
|
uniqueidentifier
|
产生这个Convoys订阅的orchestrastion的ServiceID
|
uidPropertyID1
|
uniqueidentifier
|
第一个属性的id
|
uidPropertyID2
|
uniqueidentifier
|
第二个属性的id
|
uidPropertyID3
|
uniqueidentifier
|
第三个属性的id
|
具有Convoys订阅的Orchestration,部署后,会在ConvoySets填入相关的记录,表示这个某个Convoys订阅相关的ConvoySet,和这个Convoys set对应的相关集所带的最多三个属性的属性id。
因为此表中只有三个属性的id的字段,所以限制了在Orchestration中Convoys初始化的相关集最多只能有三个属性。
看一下这个项目部署后生成的Convoys set记录:
uidConvoySetID -- f4e1e322-37ba-4fb7-a65a-692d6cd09582
nvcConvoySetName – null
uidServiceID -- 9e2c3ea7-7a5c-d8b1-0292-808c42a7f046(指向这个本项目Orchestration的id)
uidPropertyID1 -- 216903e1-07b9-49b9-bdeb-e96cc37987b8(此为属性架构PropertySchemOrderID.xsd中的属性OrderID的属性id)
uidPropertyID2 – null(初始化的相关集中只有一个属性,故后两个属性id为空)
uidPropertyID3 – null
对照上面的订阅表看一下上面的两个订阅,先看订阅名称后面是[0]的订阅相关字段:
uidServiceID -- 9e2c3ea7-7a5c-d8b1-0292-808c42a7f046(指向这个Orchestration的id)
uidInstanceID – Null(表示非实例订阅)
uidConvoySetID -- f4e1e322-37ba-4fb7-a65a-692d6cd09582(有值表示是Convoys订阅,指向这个Convoys订阅相关的ConvoySet的ID)
fCanActivateConvoySet – 1(表示这个订阅是Convoys的激活订阅)
它的订阅条件是:
http://schemas.microsoft.com/BizTalk/2003/system-properties.ReceivePortID == {0996A321-B169-4F86-A4DD-A14F8B151EED} And
http://schemas.microsoft.com/BizTalk/2003/system-properties.MessageType == http://SequentialConvoysTest.Head#Head And
http://SequentialConvoysTest.PropertySchemOrderID.OrderID Exists
订阅名称后面是[1]的订阅相关字段:
uidServiceID -- 9e2c3ea7-7a5c-d8b1-0292-808c42a7f046(指向这个Orchestration的id)
uidInstanceID – Null(表示非实例订阅)
uidConvoySetID -- f4e1e322-37ba-4fb7-a65a-692d6cd09582(有值表示是Convoys订阅,指向这个Convoys订阅相关的ConvoySet的ID)
fCanActivateConvoySet – 0(表示这个订阅是Convoys的实例订阅)
它的订阅条件是:
http://schemas.microsoft.com/BizTalk/2003/system-properties.ReceivePortID == {0996A321-B169-4F86-A4DD-A14F8B151EED} And
http://schemas.microsoft.com/BizTalk/2003/system-properties.MessageType == http://SequentialConvoysTest.Item#OrderItem And
http://SequentialConvoysTest.PropertySchemOrderID.OrderID Exists
<ns0:Head xmlns:ns0="http://SequentialConvoysTest.Head">
<OrderID>OrderID_0</OrderID>
<ItemsCount>3</ItemsCount>
<Customer>Customer_0</Customer>
<Address>Address_0</Address>
</ns0:Head>
OrderID为“OrderID_0”,表示收到了订单号为OrderID_0的订单head消息。
ItemsCount为3,表示后续有3个订单号同为“OrderID_0”的订单item消息。
消息进入到biztalk消息引擎,消息代理把这个消息跟所有订阅进行匹配,匹配到[0]订阅,订阅条件符合,不过消息代理发现这个订阅是Convoys订阅,就不会按照一般的订阅那样,直接去激活一个订阅这个消息的orchestration。下面看如何处理Convoys订阅的。
消息代理发现一个Convoys订阅([0]订阅)跟这个消息匹配,然后到Convoys set表中查找这个Convoys订阅对应的Convoys set,进而得到这个Convoys set对应的属性,本示例中[0]订阅在Convoys set表的记录只包含了一个属性:OrderID的属性id
消息代理下面从消息中获得这个OrderID属性的值,之后根据ConvoySetID和OrderID属性的值到ConvoySetInstances表中去查找,是否已经有同样的OrderID的消息已经激活了一个orchestration实例,看一下ConvoySetInstances表的结构:
字段
|
数据类型
|
含义
|
uidConvoySetID
|
uniqueidentifier
|
ConvoySetID
|
vtProp1
|
sql_variant
|
属性一的值
|
vtProp2
|
sql_variant
|
属性二的值
|
vtProp3
|
sql_variant
|
属性三的值
|
uidInstanceID
|
uniqueidentifier
|
运行在这个ConvoySet实例的orchestration服务实例id
|
uidActivatingMessageID
|
uniqueidentifier
|
激活这个ConvoySet实例的消息id
|
如果在ConvoySetInstances表中找到同样属性的值的实例,表示这个ConvoySet实例对应的orchestration实例已经存在。如果ConvoySetInstances表没有找到同样属性值的实例,表示相应的orchestration实例还未被创建。
[0]订阅的fCanActivateConvoySet字段为1,表示这是个Convoys激活订阅,所以当[0]订阅匹配的消息到来后,同时在ConvoySetInstances表没有相同的属性记录存在,则消息代理会激活一个新的orchestration实例处理此消息,同时在ConvoySetInstances表记入这个ConvoySetID此时的所有属性的值,表示这些属性值对应的实例已在运行。
看一下订单号为OrderID_0的OrderHead消息进入biztalk后,ConvoySetInstances表中新生成的记录:
如果[0]订阅的匹配的消息进来后,在ConvoySetInstances表有相同属性的记录存在,那么[0]订阅处理到此为止,不再去激活一个新orchestration实例。
l 第一个消息处理结果
第一个OrderHead消息激活orchestration实例后,由Receive_Head接收形状接收,然后往下执行Expression_1中的代码,往系统日志写入一些内容,如下:
之后再往下,初始化订单项计数器。
在循环形状中判断订单项数未满预订的数,进入循环,循环内第一个形状就是非激活接收形状,这个形状跟随了Convoys set的相关集,是Convoys的实例订阅,orchestration实例在此停留等待这个接收形状订阅的消息到来。
l orchestration实例状态
从上面的分析看,orchestration实例应该处于等待状态,过了一定时间这个orchestration实例应该转入到Dehydrated状态(即orchestration实例被序列化到数据库以节省系统资源的状态),在biztalk admin console中可以清楚的看到这个状态:
查看这个实例的详细信息:
注意Services Instance ID这个字段表示这个orchestration实例的id,看以后同一个Convoys set中的消息是否都是运行在这个orchestration实例中的。
<ns0:OrderItem xmlns:ns0="http://SequentialConvoysTest.Item">
<OrderID>OrderID_0</OrderID>
<ItemID>ItemID_1</ItemID>
<Quantity>10</Quantity>
<Note>Note_0</Note>
</ns0:OrderItem>
后续的订单项OrderItem消息开始一条一条的进入,当然OrderID都是一样为“OrderID_0”跟前面的OrderHead消息中的OrderID一致,表示同一个订单的。
这个消息进入消息代理后,跟[1]订阅匹配,这个[1]订阅是Convoys实例订阅,同样,消息代理先到Convoys set表中查找这个Convoys订阅对应的Convoys set,找到此Convoys set对应的OrderID属性,然后从消息中取出OrderID属性的值,最后到ConvoySetInstances表中去查找,结果发现ConvoySetInstances表中有这个ConvoySetID 和OrderID属性为“OrderID_0”的记录,于是消息代理把这个消息路由到这条记录中uidInstanceID字段的值6a8bc742-79c7-4024-9c07-2f502c8de723指示的那个orchestration实例。
如果OrderItem这样的Convoys实例订阅匹配到的消息,在ConvoySetInstances表没有找到相同属性的记录,就说明需要路由到某个orchestration实例的消息,结果这个orchestration实例不存在,这时消息代理会挂起消息,路由失败。
l 后续OrderItem消息处理结果
OrderItem消息被路由到前面OrderHead消息激活的那个orchestration实例中由Receive_Item接收形状接收,然后进入Expression_2运行相关代码,写入以下内容到系统日志,如下:
表示接收到了一个订单Item。
最后循环形状又把流程返回到Receive_Item接收形状继续等待接收下一个订单项消息。
l orchestration实例状态
按照上面的分析,这个订单项的消息是由OrderHead消息激活的那个orchestration实例处理的,是不是这样呢,在biztalk admin console中看运行中的服务实例还是前面那个6a8bc742-79c7-4024-9c07-2f502c8de723实例,查看这个服务实例的消息流:
可以看到,这个orchestration实例在接收了一个OrderHead消息后,接收了一个OrderItem消息,如此看来,后续的OrderItem消息的确是被路由到了前面的orchestration实例。
当最后一个OrderItem消息被orchestration实例接收到后,orchestration处理完这个消息,在循环判断退出条件满足后,退出循环,随即也就完成了整个orchestration流程,orchestration服务实例被销毁。
最后,biztalk还会把这个Convoys set在ConvoySetInstances表对应的那条记录删除。
自此,一个完整的Sequential Convoys过程结束。
经过前面比较复杂的测试过程,可以看到Convoys的消息订阅和路由跟一般的消息订阅和路由的有很大不同,下面用一个图来表示Convoys消息订阅和路由的处理流程: