《WF编程》系列之43 - 承载工作流:持久性服务 Persistence Services
2008-08-27 09:03 Windie Chai 阅读(4498) 评论(7) 编辑 收藏 举报工作流在长时间运行时难免会遇到一些问题,许多业务逻辑需要花费数日、数周乃至数月。在这段时间中,我们不能让工作流实例一直驻留在内存中(比如,我们需要一份开支报告,而不巧的是对此负责的会计师却在西班牙海滩休假,怎么办呢?)。在Windows Workflow中,可以通过持久化服务来解决长时间运行的工作流可能遇到的问题。
长时间运行的工作流耗费了大量的时间,却一直处于空闲状态。处于空闲状态的工作流可能在等待Delay活动完成,或者等待HandleExternalEvent活动的事件到达。当启用了持久化服务后,Runtime就可以将空闲的工作流持久化,然后从内存中卸载。当等待的事件到达后,Runtime再恢复工作流的执行。
什么时候来持久化工作流是由工作流Runtime决定的,而工作流状态将如何保存,还有保存到哪里则是由持久化服务决定的。空闲工作流是指没有任何活动处于执行状态的工作流,空闲工作流一般都在等待外部事件的到达或Delay活动的到期(当然,Delay活动的到期也可以看做是一种事件)。另外,当工作流满足以下条件时Runtime也会通知持久化服务去保存工作流状态:
- 当TransactionScope活动内的原子性事物或CompensatableTransactionScopeActivity活动完成时
- 当宿主应用程序调用WorkflowInstance对象的Unload或RequestPersist方法时
- 当被PersistOnClosed特性修饰的自定义活动完成时
- 当CompensatableSequence活动完成时
- 当工作流终结或完成时
译者注: 针对最后一个条件解释一下,其实持久化服务并不是要求我们必须去在工作流完成后保存状态,他只是给我们在工作流完成后进行操作的能力,该进行什么操作由我们来决定,比如下面将要介绍的SqlWorkflowPersistenceService就选择了在工作流完成之后从数据库删除这条记录,而不是保存状态。 |
6.3.1 持久化类
所有持久化服务都从WorkflowPersistenceService类继承而来。如果我们需要编写自定义持久化服务的话,就需要实现这个类定义的抽象方法。这些抽象方法在下图中用斜体字表示。此外,基类还为子类提供了一些实例方法。例如,GetDefaultSerializedForm方法以活动作为参数并返回一个bytes数组来表示序列化后的活动。如果需要序列化整个工作流,你只需要将工作流的根活动传递给这个方法。
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.3.3 SQL 持久化服务配置
当我们创建好了正确的数据库,我们就可以为工作流Runtime添加持久化服务了。我们将使用配置文件来定义服务:<configuration>
<configSections>
<section name="WorkflowWithPersistence"
type="System.Workflow.Runtime.Configuration.WorkflowRuntimeSection, System.Workflow.Runtime,Version=3.0.00000.0, Culture=neutral,PublicKeyToken=31bf3856ad364e35"/>
</configSections>
<WorkflowWithPersistence>
<CommonParameters>
<add name="ConnectionString"
value="Data Source=.;Initial Catalog=c6ps;Integrated Security=True"/>
</CommonParameters>
<Services>
<add type="System.Workflow.Runtime.Hosting.SqlWorkflowPersistenceService, System.Workflow.Runtime, Version=3.0.00000.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
UnloadOnIdle="true" />
</Services>
</WorkflowWithPersistence>
</configuration>
参数名称 | 描述 |
EnableRetries | 当值为true时,服务会重试失败的数据库操作,重试次数为20次或者一直重试到操作成功。默认值为false。 |
LoadIntervalSeconds | 服务检查过期定时器的间隔时间。默认值是120秒。 |
OwershipTimeoutSeconds | 当持久化服务加载工作流时,锁死工作流记录的时间长度(这一点在当多个Runtime公用相同的持久化服务数据库时相当重要)。默认值为TimeSpan.MaxValue。 |
UnloadOnIdle | 当设置为true时,服务会持久化并从内存中卸载空闲的工作流。默认值是false。 |
6.3.4 运行持久化服务
我们将使用如下的工作流定义来是持久化服务生效: x:Name=" WorkflowWithDelay"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/workflow">
<CodeActivity x:Name="codeActivity1" ExecuteCode="codeActivity_ExecuteCode" />
<DelayActivity TimeoutDuration="00:00:10" x:Name="delayActivity1" />
<CodeActivity x:Name="codeActivity2" ExecuteCode="codeActivity_ExecuteCode" />
</SequentialWorkflowActivity>
{
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 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<WorkflowEventArgs>(
runtime_WorkflowPersisted);
runtime.WorkflowLoaded +=
new EventHandler<WorkflowEventArgs>(
runtime_WorkflowLoaded);
runtime.WorkflowUnloaded +=
new EventHandler<WorkflowEventArgs>(
runtime_WorkflowUnloaded);
runtime.WorkflowIdled +=
new EventHandler<WorkflowEventArgs>(
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);
}
}

第一个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类。如果我们需要编写如下的工作流的话,更加需要牢记这一点:
{
Bug _bug = new Bug();
}
class Bug
{
private Guid _id;
public Guid BugID
{
get { return _id; }
set { _id = value; }
}
}
BinarryFormatter会尝试序列化工作流状态信息的所有内容,当然也包括这个自定义的Bug对象。当任何对象格式化器(object formatter)发现需要序列化的对象时,它首先会查看该对象类型是否注册了代理选择器(surrogate selector)。如果没有代理选择器,格式化器就会查看Type是否标记了Serializable特性。如果这些条件都不满足,格式化器就会停止工作并抛出异常。Bug类是我们自己添加的类,我们可以用Serializable特性来装饰它,藉此来避免发生异常。
class Bug
{
private Guid _id;
public Guid BugID
{
get { return _id; }
set { _id = value; }
}
}
如果有一些字段并不需要和工作流实例一起序列化和恢复,我们就可以通过NonSerialized特性来告诉格式化器跳过它。在下面的代码中,Bug对象就不会被序列化。当Runtime重新加载了工作流后,_bug字段又会变成初始状态。
{
[NonSerialized]
Bug _bug = new Bug();
}
译者注: 关于持久化,园子里还有几位朋友的文章也相当不错,推荐阅读: |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构