《WF编程》系列之43 – 承载工作流:持久性服务 Persistence Services

工作流在长时间运行时难免会遇到一些问题,许多业务逻辑需要花费数日、数周乃至数月。在这段时间中,我们不能让工作流实例一直驻留在内存中(比如,我们需要一份开支报告,而不巧的是对此负责的会计师却在西班牙海滩休假,怎么办呢?)。在Windows Workflow中,可以通过持久化服务来解决长时间运行的工作流可能遇到的问题。

长时间运行的工作流耗费了大量的时间,却一直处于空闲状态。处于空闲状态的工作流可能在等待Delay活动完成,或者等待 HandleExternalEvent活动的事件到达。当启用了持久化服务后,Runtime就可以将空闲的工作流持久化,然后从内存中卸载。当等待的事件到达后,Runtime再恢复工作流的执行。

什么时候来持久化工作流是由工作流Runtime决定的,而工作流状态将如何保存,还有保存到哪里则是由持久化服务决定的。空闲工作流是指没有任何活动处于执行状态的工作流,空闲工作流一般都在等待外部事件的到达或Delay活动的到期(当然,Delay活动的到期也可以看做是一种事件)。另外,当工作流满足以下条件时Runtime也会通知持久化服务去保存工作流状态:

  • 当TransactionScope活动内的原子性事物或CompensatableTransactionScopeActivity活动完成时
  • 当宿主应用程序调用WorkflowInstance对象的Unload或RequestPersist方法时
  • 当被PersistOnClosed特性修饰的自定义活动完成时
  • 当CompensatableSequence活动完成时
  • 当工作流终结或完成时

最后一个条件可能会有点奇怪,因为工作流完成或被终结之后就没有必要去重新加载工作流了,那么为什么还要保存状态呢?持久化服务可以使用这个机会去清空上一次操作遗留的工作流状态。持久化服务可以将工作流状态作为一条记录保存到数据库中,例如,在工作流完成后,持久化服务可以删除这条记录,也可以保留着完成状态的记录作为归档。

译者注:针对最后一个条件解释一下,其实持久化服务并不是要求我们必须去在工作流完成后保存状态,他只是给我们在工作流完成后进行操作的能力,该进行什么操作由我们来决定,比如下面将要介绍的SqlWorkflowPersistenceService就选择了在工作流完成之后从数据库删除这条记录,而不是保存状态。

6.3.1 持久化类

所有持久化服务都从WorkflowPersistenceService类继承而来。如果我们需要编写自定义持久化服务的话,就需要实现这个类定义的抽象方法。这些抽象方法在下图中用斜体字表示。此外,基类还为子类提供了一些实例方法。例如,GetDefaultSerializedForm方法以活动作为参数并返回一个bytes数组来表示序列化后的活动。如果需要序列化整个工作流,你只需要将工作流的根活动传递给这个方法。

6_6_PersistenceServiceClass

Windows Workflow提供了一个现成的持久化服务 – SqlWorkflowPersistenceService。SQL持久化服务将工作流状态保存到Microsoft SQL Server数据库中,接下来我们来认识一下它。

6.3.2 SqlWorkflowPersistenceService

想要使用SQL持久化服务,我们还需要一个数据库。我们可以使用已经存在的数据库,或者创建一个新的数据库。创建数据库的方法有好多,比如企业管理器、查询分析器、SQL Server 2005的SQL Server Management Studio、SQL Server 2005的命令行工具sqlcmd.exe和SQL Server 2000的osql.exe。当数据库创建后,我们需要运行Windows Workflow SQL持久化服务的脚本来创建持久化服务需要的数据库对象。这些脚本位于Windows Workflow安装目录中(也就是.NET 3.0 Runtime安装目录中):
C:\WINDOWS\Microsoft.NET\Framework\v3.0\Windows Workflow Foundation\SQL\EN。
需要执行的SQL脚本有两个:SqlPersistenceService_Schema.sql和 SqlPersistenceService_Logic.sql。首先执行schema脚本来创建表和索引,然后再执行Logic脚本来创建一些存储过程。下图演示了如何使用sqlcmd.exe创建持久化服务数据库。首先我们创建数据库,然后通过sqlcmd.exe加:r参数来运行两个脚本。

6_4_CreatePersistenceDatabase

6.3.3 SQL 持久化服务配置

当我们创建好了正确的数据库,我们就可以为工作流Runtime添加持久化服务了。我们将使用配置文件来定义服务:


持久数据库连接字符串位于CommonParameters节中,这样做的好处是可以将连接字符串共享给其它需要连接该数据库的服务。在Services 节中的SqlWorkflowPersistenceService节包含一些参数来调整服务的行为。本例中,我们只配置了一个参数 – UnloadOnIdle。它的其它参数如下表所示:

参数名称 描述
EnableRetries 当值为true时,服务会重试失败的数据库操作,重试次数为20次或者一直重试到操作成功。默认值为false。
LoadIntervalSeconds 服务检查过期定时器的间隔时间。默认值是120秒。
OwershipTimeoutSeconds 当持久化服务加载工作流时,锁死工作流记录的时间长度(这一点在当多个Runtime公用相同的持久化服务数据库时相当重要)。默认值为TimeSpan.MaxValue。
UnloadOnIdle 当设置为true时,服务会持久化并从内存中卸载空闲的工作流。默认值是false。

6.3.4 运行持久化服务

我们将使用如下的工作流定义来是持久化服务生效:


在两个Code活动之间有一个Delay活动。Delay活动会让工作流空闲10秒钟。在后台代码中,两个Code活动的ExecuteCode事件都引用了同一个事件处理器,用来控制台窗口中输入简单的信息。
namespace Chapter6_PersistenceService
{
public partial class WorkflowWithDelay: SequentialWorkflowActivity
{
private void codeActivity_ExecuteCode(object sender, EventArgs e)
{
CodeActivity activity = sender as CodeActivity;
Console.WriteLine("Hello from {0}", activity.Name);
}
}
}

接着,我们订阅一些事件并让宿主程序运行工作流。
public class Persist
{
public static void Run()
{
using (WorkflowRuntime runtime = new WorkflowRuntime("WorkflowWithPersistence"))
using (AutoResetEvent reset = new AutoResetEvent(false))
{
runtime.WorkflowCompleted += delegate { reset.Set(); };
runtime.WorkflowTerminated += delegate { reset.Set(); };
runtime.WorkflowPersisted +=
new EventHandler(
runtime_WorkflowPersisted);
runtime.WorkflowLoaded +=
new EventHandler(
runtime_WorkflowLoaded);
runtime.WorkflowUnloaded +=
new EventHandler(
runtime_WorkflowUnloaded);
runtime.WorkflowIdled +=
new EventHandler(
runtime_WorkflowIdled);
WorkflowInstance instance;
instance = runtime.CreateWorkflow(typeof(WorkflowWithDelay));
instance.Start();
reset.WaitOne();
}
}
static void runtime_WorkflowIdled(object sender, WorkflowEventArgs e)
{
Console.WriteLine("Workflow {0} idled",
e.WorkflowInstance.InstanceId);
}
static void runtime_WorkflowUnloaded(object sender, WorkflowEventArgs e)
{
Console.WriteLine("Workflow {0} unloaded",
e.WorkflowInstance.InstanceId);
}
static void runtime_WorkflowLoaded(object sender, WorkflowEventArgs e)
{
Console.WriteLine("Workflow {0} loaded",
e.WorkflowInstance.InstanceId);
}
static void runtime_WorkflowPersisted(object sender, WorkflowEventArgs e)
{
Console.WriteLine("Workflow {0} persisted";,
e.WorkflowInstance.InstanceId);
}
}

宿主应用程序通过配置文件创建了新的Runtime,然后订阅了一些工作流事件来向控制台窗口中输出信息。当上面的代码执行完后,我们的实例程序将如下图所示:
6_5_WorkflowWithPersistence

第一个Code活动执行后,在控制台中输出了信息。然后Delay活动阻止了工作流。Runtime发现工作流已经没有可执行的操作就触发了 WorkflowIdled事件。同时Runtime还发现持久化服务被启用了,而且该服务被配置为UnloadOnIdle,于是Runtime通知持久化服务去保存工作流的状态,然后卸载了工作流实例。当Delay活动过期后,Runtime使用持久化服务来重新加载工作流实例并恢复执行。

当SQL持久化服务重新加载了工作流之后,服务会在数据库中标记实例为锁死状态。如果其它进程的持久化服务或者其它计算机想要加载这个工作流,服务就会抛出异常。锁的作用就是防止工作流实例在两个不同的Runtime中运行。当工作流再一次被持久化后,或者锁超时后(可以通过 OwnershipTimeoutSeconds设置锁死长度),锁就会被解开。

当工作流完成后,Runtime再一次通知持久化服务去持久化工作流。SqlWorkflowPersistenceService在对工作流进行“考察”后,发现工作流确实已经完成了执行过程。这时服务并不会保存当前状态,而是会删除上一次保存的状态记录。在持久化服务数据库中,大多数的数据库操作发生在InstanceState表中。

持久化服务保存工作流状态时,第一个工作就是将工作流序列化。为了更好的理解持久化服务,我们再来看看WF中的序列化。

6.3.5 持久化和序列化

在Windows Workflow中有两种类型的序列化。Runtime提供了WorkflowMarkupSerializer类来将工作流转换为XAML。标记语言序列化器并不关心工作流内部的数据,也不需要保存状态。它的目的仅仅是为设计工具和代码生成器生成XML格式的工作流定义。

而持久化服务所使用的是另外一种序列化方式,持久化服务通过使用WorkflowPersistenceService类的 GetDefaultSerializeForm方法来序列化工作流。这个方法调用了Activity类的Save方法,Save方法通过使用 BinaryFormatter对象(二进制格式化器)来序列化工作流。二进制格式化器会生成一个字节流,WorkflowPersistenceService会通过GZipStream来压缩这个字节流。二进制序列化的目的是生成工作流实例的紧凑的表现形式,然后用来长时间存储。现在我们知道了Windows Workflow中有两种不同类型的序列化器,但它们的功能却不重复,因为从根本上来说,它们的应用场景和目的就是不一样的。

我们没有必要去理解完整的序列化细节,重要的是要知道当持久化工作流时,Runtime会使用BinaryFormatter类。如果我们需要编写如下的工作流的话,更加需要牢记这一点:
public partial class WorkflowWithDelay2 : SequentialWorkflowActivity
{
Bug _bug = new Bug();
}
class Bug
{
private Guid _id;
public Guid BugID
{
get { return _id; }
set { _id = value; }
}
}

假设这个工作流除了和上面的示例一样包含一个Delay活动之外,还包含一个私有的Bug对象。哪怕我们对Bug对象并不感兴趣,它也会改变持久化行为。如果我们不启用持久化服务,那么这个工作流会成功的完成。但如果我们启用了持久化服务,我们就会发现一个异常:Type Bug is not marked as serializable。
BinarryFormatter会尝试序列化工作流状态信息的所有内容,当然也包括这个自定义的Bug对象。当任何对象格式化器(object formatter)发现需要序列化的对象时,它首先会查看该对象类型是否注册了代理选择器(surrogate selector)。如果没有代理选择器,格式化器就会查看Type是否标记了Serializable特性。如果这些条件都不满足,格式化器就会停止工作并抛出异常。Bug类是我们自己添加的类,我们可以用Serializable特性来装饰它,藉此来避免发生异常。
[Serializable]
class Bug
{
private Guid _id;
public Guid BugID
{
get { return _id; }
set { _id = value; }
}
}

如果Bug类是第三方提供的,我们可以编写一个代理选择器来完成序列化,不过这已经超出了本章讨论的范畴(关于SurrogateSelector类的更多细节请查看System.Runtime.Serialization命名空间)。

如果有一些字段并不需要和工作流实例一起序列化和恢复,我们就可以通过NonSerialized特性来告诉格式化器跳过它。在下面的代码中,Bug对象就不会被序列化。当Runtime重新加载了工作流后,_bug字段又会变成初始状态。
public partial class WorkflowWithDelay2 : SequentialWorkflowActivity
{
[NonSerialized]
Bug _bug = new Bug();
}

持久化服务保存工作流状态,并允许我们重启计算机和长时间的运行工作流。但是,持久化服务并不能告诉我们在工作流的执行过程中发生了什么,或者工作流已经进行多久了。我们的下一个主题 – 跟踪服务会给予我们这方面的信息。

译者注: 关于持久化,博客园有几位朋友的文章也相当不错,推荐阅读:

One Comment

发表评论

电子邮件地址不会被公开。 必填项已用*标注