【翻译】WF从入门到精通(第十五章):工作流和事务
学习完本章,你将掌握:
1.了解传统的事务模型以及这种模型在哪些地方适合去使用,哪些地方不适合使用
2.懂得在哪些地方不适合传统的事务以及什么时候是补偿事务的恰当时机
3.看看怎样回滚或补偿事务
4.看看怎样修改默认的补偿顺序
如果你是写软件的,你迟早需要去理解事务处理。事务处理(transactional processing)在这个意义上是指写那些把信息记录到一个持久化资源的软件,这些持久化资源如数据库、Microsoft消息队列(它在底层使用了一个数据库)、带事务文件系统的Windows Vista以及注册表存取或者甚至是其它一些支持事务处理的软件系统。持久化资源不管发生什么事情,一旦数据被记录下来就会保留这些写入的信息。
事务对于任何业务流程都是关键的,因为通过事务,你能确保你的应用程序中数据的一致性。假如业务流程维持一个错误但仍要持久化一些数据,那么这些错误的数据很有可能会波及到整个系统,这就留给你一个问题:哪些数据是正确的,哪些数据是错误的。试想从一个在线商家订购这本书,可是却发现商家和你的信用卡交易“出了点小意外”,收取了你书的标价的100倍而不是他们的折扣价。当像这样的错误可能发生时,事务处理就不再是一个可笑的或者避而不谈的话题。
理解事务
事务处理,其核心就是管理你的应用程序状态。对于状态,我实际指的是应用程序的所有数据的状况。当应用程序的所有数据是一致的,那么该应用程序就处于一个确定状态。假如你把一条新的客户记录添加到你的数据库中,则此过程需要两个插入(一是新增一条通常的包含有把地址和你的客户联系在一起的信息的行记录,另一条是记录真实地址信息),添加通常的行记录成功后,但添加它的地址时却失败了,这就使你的应用程序处于一个不确定状态。当某人试图检索该地址将发生什么呢?系统会提示到地址应该在此,但真实的地址记录已丢失。你的应用程序数据现在是不一致的。
为确保这两个操作成功,事务起到了作用。一个事务本身是一个单一的工作单元,它要么全部成功要么全部失败。这并不是说你不能更新两个不同的数据库表。它刚刚的意思是两个表的更新被看作是一个单一的工作单元,两个都必须被更新否则都失败。假如其中一个或者两个更新失败,理想情况下是你想让系统回到刚刚你试图更新这些表之前的状态。没有迹象表明试图更新这些表的操作是非成功的话,你的应用程序就应该继续前进,但更重要的是,你不想有来自更新不成功的一个表里有而另一个表里却没有的数据。
备注:有整卷书全部写和事务及事务处理相关知识的书籍。尽管我将描述的解释Microsoft Windows Workflow Foundation(WF)是怎么支持事务的相关概念足够深,但我不可能在本书中以相当深的深度覆盖事务处理的方方面面。假如你还没有重新看看.NET 2.0中通常的事务支持的话,你应该这样去做。WF的事务处理模式和.NET 2.0的事务支持非常紧密,你可以在下面的论文中找到有用的知识去理解WF的事务支持:msdn2.microsoft.com/en-us/library/ms973865.aspx。
传统上,事务来自于一个单一的模式:XA或两阶段提交(two-phase commit)类型的事务。但是,随着基于Internet通信的出现以及需要提交长时间运行的事务的要求,引进了新一些的称作补偿式事务的事务类型。WF支持这两种类型。我们将首先讨论传统的事务,然后我们将提到使用这种类型的事务是一个低级的架构选择,再后我们将讨论补偿式事务。
传统(XA)事务
已知的第一个实现了事务处理的系统是一个航空公司的预订系统。对于需要预订多个航班的请求,假如任何一个单独的航班不能预订,则该预订就不能进行。系统设计师知道这些并设计了一个我们今天称为X/Open分布式事务处理模型,简称XA的事务化方式来进行处理。(看看en.wikipedia.org/wiki/X/Open_XA。)
XA事务涉及到XA协议,即我先前提到的两阶段提交和三个实体:应用程序、资源和事务管理器。应用程序也就是指你的应用程序。资源是一个设计的用来加入到XA类型的事务中去的软件系统,也就是说它参与事务并懂得怎样参与两阶段提交数据以及提供持久性(很快将进行讨论)。事务管理器监视整个事务处理流程。
因此什么是两阶段提交呢?最后,想像你的应用程序需要写入数据,也就是说一个数据库。假如写入过程在事务下执行,数据库就保持这些要被写入的数据直到事务管理器发出一条准备(prepare)指令为止。在那时,数据库以一个表决(vote)作为响应。假如该表决是要前进并把数据提交(写入)到表中,那么事务管理器则进入并参与资源,假如有的话。
假如所有资源对提交数据都表决成功,则事务管理器发出一个提交(commit)指令然后每一个资源就把数据写到它内部的数据存储中。只有到那时指定要写入你的表中的数据才真正地插入到数据库中。
假如任何一个资源有问题并且没有对提交数据进行表决,则事务管理器发出一个回滚(rollback)指令。所有参与进事务的资源就必须销毁和事务相关的信息,没有东西被永久的记录保存下来。
一旦数据被提交,XA协议会保证事务结果的持久性。数据是一致的,并且应用程序也处在一个确定状态中。
ACID属性
当我们谈到XA事务时是不可能不提到ACID的:原子性、一致性、隔离性和持久性(en.wikipedia.org/wiki/ACID)。
对于原子性(atomic),我们指的是资源参与了两阶段提交协议的事务支持中。要被处理的数据要么全部被处理(更新、删除或者其它任何操作)要么都不处理。假如事务失败,资源就返回到刚刚要试图处理该数据之前的状态。
一致性(Consistency)指的是数据保持完整性。对数据库而言,典型的意思是指数据不应违反任何一致性约束,但对于其它资源而言,保持完整性可能有所区别或者有额外的含义。假如数据违反了任何规则或者一致性约束,它最后的结果会是应用程序处于一个不确定状态中,资源必须回滚事务以防止不一致的数据被持久记录进系统中。
隔离性(isolation)是指在事务进行中导致系统不能存取数据的一个事务属性。在数据库中,试图写入一个之前被锁住的行或者读取一个未提交数据的行是不允许的。仅当数据被提交后才能使用这些数据,或者是在你明确允许未提交读时进行读操作的情况(通常称作“脏数据”)。
持久性(durable)资源保证数据被提交后将总是能以非易失性的方式获取它。如果数据库服务器的电源在数据被提交后的一毫秒被切断,在数据库服务器重新上线后数据应还在数据库中为你的应用程序的使用随时做好准备。在实际中做到这些比听起来还更加困难,一个主要的原因是架构师使用数据库来为关键的数据进行持久化的数据存储而不是像XML之类的单一数据文件。(不可否认,Windows Vista中的事务文件系统有了一些改观,但希望你能领会我的观点。)
长时间运行的处理过程和应用程序状态
记住,XA类型的事务的整个前提是假如事务回滚的话,你的应用程序将回到它的初始状态。但是考虑一下这个情况:假如事务花费了过长的时间才提交的话你的应用程序会发生什么呢?
在我回答之前,假想你的在线交易系统收到了一个客户的订单,但是信用卡验证的过程中被中断了。无疑你的处理过程在一个事务中进行,因为假如某些地方失败的话你就不想再去收取该客户的款项。但是与此同时,其它客户又正在发来订单,假如你运气不错的话,这是大量的订单。假如第一个客户的交易失败后,订单在此期间发来将发生什么呢?
假如系统的目的不是为了隔离单独的一个订单处理失败,那么正确的做法是把系统完全回滚到它的原始状态。但是考虑这种情况则意味着,我们不仅会丢失第一个客户的订单,也会丢失在此期间其它每一个客户发来的订单。即使这可能只有两个订单,但也不是很好。假如这是10,000份订单呢...损失的收入数额是不能容忍的。
当然,我们将保留这10,000份订单并把刚刚处理的第一份订单放到一个孤立的事件中,但是在这种情况下我们同时也有可能会有意破坏事务属性中的某一个来保留这些订单收入。这是一个风险,需要进行估量,但通常在现实世界情况下我们必须接受这一风险。
被破坏的属性其实是原子性,出于这个原因,写事务处理系统的人会努力在尽可能短的时间内持有事务。你做的仅仅只能是你事务范围内所必须要做的事,你要尽可能高效地做这些事以便事务快速地完成。
现在我们进入另外一种复杂情况:Internet。你的客户正在线发送订单,网络速度是出了名的慢甚至中断。因此,在Internet之上的事务处理是有疑虑的。
补偿作为一个解决办法
这正是创建所需要的补偿事务的情况。假如我给你五个苹果,过程使用XA-类型的事务,事务失败时,时间会回滚到我开始给你苹果的那一刻。在某种意义上说,历史会被重写以致这五个苹果没有被首先考虑到。但是假如我是在一个补偿式事务中给你的五个苹果的话,事务失败进行补偿(以便我们维护一个确定的应用程序状态)时,你必须返回五个苹果给我。看起来差别很小,但两种类型的事务之间存在明显的区别。
当写XA类型的事务时,负责回滚失败事务的职责落在资源上,例如你的数据库。相反,当一个补偿式事务失败时,作为事务参与者,你有责任通过提供一个事务补偿功能来为你的事务部分提供补偿。假如你扣除了某个在线客户信用卡中的金额并在后来被告知要补偿,你会立即向该客户账户存入你起初扣除的相同数目的金额。在一个XA类型的事务中,该账户绝不会在一开始就被扣除。对于补偿式事务来说,你发起两个操作:一个是扣除账户,另一个是后来存入它。
备注:毫无疑问,它将是一个极好的系统,它能在Internet上成功执行XA类型的事务。但是要非常仔细地构思你的补偿功能,注意各个细节。如果你不这样做的话,你可能由于一错再错而使情况更加糟糕。写出准确的补偿功能通常并不容易。
向你的工作流中引入事务
总的来说,在WF中引入事务和拖拽一个基于事务的活动到你的工作流中一样简单。但是,如果你正使用事务活动话,你应该知道更多一些的东西。
工作流运行时和事务服务
当你在你的工作流中使用基于事务的活动时,需要两个可插拔的工作流服务。首先,因为基于事务的WF活动需要标注PersistOnClose特性(在第6章“加载和卸载实例”中提过),因此你也必须启动SqlWorkflowPersistenceService。假如你没有的话,WF不会崩溃,但是你的事务也不会提交。
或许在本章更感兴趣的是DefaultWorkflowTransactionService。 这个服务为你事务操作的启动和提交负责。没有这样一个服务的话,工作流运行时内的事务是不可能实现的。
备注:你可以创建你自己的事务服务,尽管这超出了本章的范围。所有WF事务服务都派生自WorkflowTransactionService,因此创建你自己的服务基本上就是重写(override)你想改变的基类功能。实际上,WF用了一个自定义的事务服务:SharedConnectionWorkflowTransactionService来承载共用的Microsoft SQL Server连接。在msdn2.microsoft.com/en-us/library/ms734716.aspx可找到更详细的信息。
失败处理
对于事务失败,尽管在你的工作流中并不需要你去处理失败过程,但这是个好习惯。我不想提它的原因是它被认为是一个最佳做法。我提到它的原因是你可能去实现你自己的,在真正失败之前能自动检查异常并重新尝试事务处理的事务服务。尽管对这些要怎么做进行演示超出了本章的范围,但你应该知道这是可以做到的。
环境事务(ambient transaction)
基于事务的活动都在被称为环境事务的东西下工作。当你的工作流进入一个事务范围内的时候,工作流事务服务会自动为你创建一个事务。这不需要去尝试并建立你自己的事务。所有嵌入到事务范围中的活动都属于这一个环境事务,假如事务成功它们就都提交,否则就回滚(或者补偿)。
使用TransactionScope活动
在WF中XA类型的事务由TransactionScope活动实现。这个活动和.NET的System.Transactions名称空间的联系密切,事实上当活动开始执行的时,它会创建一个Transaction来作为环境事务。该TransactionScope活动甚至与System.Transactions共享数据结构(TransactionOptions)。
使用基于组合活动的TransactionScope确实和把它拖到你的工作流中一样容易。你放进TransactionScope活动中的任何活动都自动继承该环境事务并像通常的使用.NET自己的System.Transactions事务时一样进行操作。
备注:你不能在其它事务活动中放入TransactionScope活动。事务嵌套是不允许的。(这条规则也适用于CompensatableTransactionScope。)
事务选项更精确地指定了环境事务将怎样进行操作。由System.Transactions.TransactionOptions结构支持的这些选项让你能去设置环境事务将支持的隔离级别和延时时间。延时时间值可不需加以说明,但是隔离级别则不。
备注:超时值可以进行限制,它是可配置的。有一个机器范围(machine-wide)的设置System.Transactions.Configuration.MachineSettingsSection.MaxTimeout,和一个局部范围的设置System.Transactions.Confituration.DefaultSettingsSection.Timeout。它们能为超时值设置一个最大上限值。这些值会覆盖你使用TransactionOptions所做的任何设置。
事务隔离级别在很大程度上确定了事务能处理哪些要进行事务处理的数据。例如,你或许想让你的事务能去读取未提交的数据(解除前面的事务数据库页面锁)。或者你正写入的数据可能很关键,因此你要让事务只能读取已提交的数据,甚至,当你的事务正执行时,你禁止让其它事务在该数据上工作。你可以选择使用的隔离级别如表15-1所示。使用TransactionScope活动的TransactionOptions属性,你可同时设置隔离级别和超时值。
表15-1 事务的隔离级别
隔离级别 | 含义 |
Chaos | 无法改写隔离级别更高的事务中的挂起的更改。 |
ReadCommitted | 在正在读取数据时保持共享锁,以避免脏读,但是在事务结束之前可以更改数据,从而导致不可重复的读取或幻像数据。即在事务期间未提交的数据不能读出,但能被修改。 |
ReadUncommitted | 可以进行脏读,意思是说,不发布共享锁,也不接受独占锁。即在事务期间未提交的数据既能读出也能修改。要记住数据可以修改:不能保证该数据和随后读取的数据是相同的。 |
RepeatableRead | 在查询中使用的所有数据上放置锁,以防止其他用户更新这些数据。防止不可重复的读取,但是仍可以有幻像行。即在事务期间未提交的数据能读出但不能修改。但是,能插入新数据。 |
Serializable | 在DataSet上放置范围锁,以防止在事务完成之前由其他用户更新行或向数据集中插入行。未提交的数据能读出但不能修改,在事务期间不能插入新数据。 |
Snapshot | 通过在一个应用程序正在修改数据时存储另一个应用程序可以读取的相同数据版本来减少阻止。表示您无法从一个事务中看到在其他事务中进行的更改,即便重新查询也是如此。即未提交的数据能读出,但在事务真正修改数据之前,该事务会验证在它最初读取数据后其它事务没有修改该数据。假如数据被修改了,该事务就激发员工错误。这样做的目的是让事务能读取先前提交的数据值。 |
Unspecified | 正在使用与指定隔离级别不同的隔离级别,但是无法确定该级别。当使用OdbcTransaction时,如果不设置IsolationLevel或者将IsolationLevel设置为Unspecied,事务将根据基础ODBC驱动程序的默认隔离级别来执行。假如你试图设置事务隔离级别为该值,那么就会抛出一个异常。只有事务系统能设置该值。 |
提交事务
假如你正使用的是SQL Server事务,或者可能是COM+事务,你就知道一旦数据已被插入、更新或删除,你就必须提交该事务。也就是说,你启动两阶段提交协议和数据库来永久记录或移除这些数据。
但是,对于TransactionScope活动而言不是必要的。假如事务执行成功(当插入、更新、删除数据时没有出错),当工作流执行过程离开该事务范围时,该事务会为你自动提交。
回滚事务
怎么样回滚失败的事务呢?哦,就像事务成功为你提交一样,假如事务失败数据也将被回滚。有趣的是回滚是悄无声息的,至少就WF而言是很关注的。假如你需要检查事务是成功还是失败,你就需要亲自加入逻辑代码来完成这件事。假如事务失败,TransactionScope是不会自动抛出异常的。它仅仅回滚数据然后继续前进。
使用CompensatableTransactionScope活动
假如XA类型的事务不能胜任,你可以拖拽CompensatableTransactionScope活动到你的工作流中来提供补偿式的事务处理过程。CompensatableTransactionScope活动和TransactionScope一样是一个组合活动。但是,CompensatableTransactionScope实现了ICompensatableActivity接口,通过实现Compensate方法赋予了它能对失败的事务进行补偿的能力。
和TransactionScope相似,CompensatableTransactionScope活动也创建一个环境事务。包含进CompensatableTransactionScope的活动共享这个事务。假如它们操作都成功,数据就被提交。但是,如果它们中任何一个操作失败,你通常通过执行一个Throw活动去启动补偿。
提示:补偿式事务能支持像数据库之类的传统资源,当事务提交时,数据就像以XA类型的事务一样被提交。但是,补偿式事务的优点是你不必选择一个XA类型的资源来存储数据。对于不支持传统资源的一个典型例子是使用一个Web服务来把数据发送到一个远程站点。假如你把数据发送到远程站点但是后来必须进行补偿的话,你需要和数据不再有效的远程站点间进行某种通信。(你怎么实现这些有赖于各个远程站点。)
Throw导致死亡失败并为你的CompensatableTransactionScope活动去执行你的补偿处理程序。你可通过CompensatableTransactionScope活动的智能标签来访问该补偿处理程序,在许多地方和你添加一个FaultHandler是一样的。
备注:尽管抛出一个异常启动事务补偿,但是Throw活动自身并不会进行处理。你也能在你的工作流中放入一个FaultHandler活动来防止工作流提前终止。
使用Compensate活动
当你通过CompensatableTransactionScope对失败进行补偿时,会调用补偿处理程序。假如你有多个补偿式事务时,事务将以默认的顺序进行补偿,从嵌套得最深的事务开始向外工作。(在下一节中你将看到这些怎么实现。)当你的处理逻辑要求补偿的时候,你可以在你的补偿处理程序中放入一个Compensate活动来开始为所有已经完成的、支持ICompensatableActivity的活动进行补偿。
实际上异常的情况总是会引起补偿,因此使用Compensate活动不是必须的。为什么还要用它呢?因为你可能在一个CompensatableSequence活动中嵌入超过一个以上的补偿式事务。假如一个事务失败并去补偿的话,你就能为其它事务开始补偿工作,即使该事务先前已经成功完成。
备注:Compensate活动只有在补偿处理程序(cmpensation handler)、取消处理程序(cancellation handler)和失败处理程序(fault handler)中有效。
只有当你需要以某个其它的顺序而不是默认的补偿顺序进行补偿的时候,你才应该去使用Compensate活动。默认补偿的顺序和所有嵌入的ICompensatableActivity活动的完成顺序正好相反。假如这个顺序不适合你的工作流模型,或者假如你想要有选择性地为已完成的可补偿子活动进行补偿的话,Compensate活动是一个可选择的工具。
备注:Compensate活动使用它的TargetActivityName属性来识别出哪个可补偿式活动应该被补偿。假如超过一个以上的可补偿式活动要进行补偿工作的话,你就需要使用一个以上的Compensate活动。假如你决定不去补偿一个特定的事务的话,对于该事务或者包含其父活动中的补偿处理程序中可不做任何事情。
通过让你能决定你是否想立即对支持补偿的子活动进行补偿的这一手段,Compensate活动为你提供了在补偿处理过程中进行控制的能力。这个能力能让你的工作流按照你业务流程的需要在一个嵌套的补偿式活动中明确无误地执行补偿工作。通过在Compensate活动中指定你想要去补偿的可补偿式活动,只要该可补偿式活动先前已成功的提交,那么该可补偿式活动中的任何补偿代码都将被执行。
假如你想对超过一个以上的可补偿式活动进行补偿的话,你可在你的处理程序中为每一个你想去补偿的可补偿式活动添加一个Compensate活动。假如Compensate活动用在了一个可补偿式活动的处理程序中,而该可补偿式活动的又容纳有嵌入的可补偿式活动,并且假如为该Compensate活动指定的TargetActivityName是其父活动的话,在该父活动中所有已经成功提交的子(可补偿式)活动的补偿工作也都会被调用。为了强调这些共说了三次。
使用CompensatableSequence活动
前一节可能留给你一个疑问:Compensate活动以什么为载体。毕竟,你不能嵌入补偿式事务,你不能嵌入基于WF的任何一种类型的事务。
但是让我们从不同的角度来看待它。你会怎样把两个可补偿式事务捆在一起以便其中一个失败时触发另一个去补偿,而且假如另一个已经成功完成了呢?答案是你成对地把补偿式事务放进一个单一的CompensatableSequence活动中。然后,假如两个子事务活动中的其中一个失败的话,在CompensatableSequence活动的补偿代码或者失败处理程序中,你就触发这两个子事务活动去进行补偿。甚至更有趣的是,在你把三个补偿式事务一起捆进一个单一的CompensatableSequence活动中,并且允许即使其它两个事务失败并被补偿的情况下,该事务也能成功。Compensate活动为你提供了这些控制能力。
这些突出了CompensatableSequence活动的作用。CompensatableSequence活动从本质上来说是一个Sequence活动,你使用CompensatableSequence活动的方式和任何顺序活动一样。主要的区别是你能在一个单一的CompensatableSequence活动中嵌入多个可补偿式活动,实际上是把相关的事务捆在一起。让CompensatableSequence活动结合CompensatableTransactionScope和Compensate活动能为你在你的工作流中提供强大的事务控制能力。
备注:CompensatableSequence活动能被嵌进其它的CompensatableSequence活动中,但是它们不能作为CompensatableTransactionScope活动的子活动。
提示:当在单一的一个可补偿式序列中包含多个补偿式事务的时候,你不需要为个别事务活动指定补偿功能。如果需要的话补偿会流向父活动,因此假如你需要的话,你能把你的补偿活动都聚集到一个封闭的可补偿式顺序活动中。
创建一个事务型的工作流
我已创建了一个模拟自动柜员机(ATM)的应用程序,你提供你的个人识别码(也称作PIN)后,就可从你的银行账户中存款和取款。存款操作将被嵌进一个XA类型的事务中,而取款过程如果操作失败的话将进行补偿。为了对应用程序的事务性质进行练习,我在该应用程序中放入了一个“Force Transactional error”复选框(check box)。只要简单地选中该复选框,接下来相关的数据库操作将失败。对于该应用程序的工作流来说是基于状态的,它比你在前一章(第14章,“基于状态的工作流”)中看到过的应用程序还要复杂。我在图15-1中展示了该状态机工作流。应用程序的大部分代码我已经为你写出了。接下来你将在练习中添加事务组件。
图15-1 WorkflowATM的状态图
该应用程序的用户界面如图15-2所示。这是应用程序的初始状态,插入银行卡之前ATM的状态都和这近似。当然,该示例不能处理真实的银行卡,因此点击B键将把用户界面(和应用程序状态)切换到PIN验证状态(如图15-3所示)。
图15-2 WorkflowATM的初始用户接口
图15-3 WorkflowATM的PIN验证用户界面
你使用按键输入正确的PIN。一旦输入了四个数字代码,你可点击C键来开始对数据库进行查询以验证该PIN。假如PIN经过验证(注意帐号在左下角,该PIN码必须是该帐号的),用户界面便切换到如图15-4所示的操作选择状态。这里你可选择从你的账户中存款或取款。
图15-4 WorkflowATM的操作选择用户界面
对于存款和取款的应用程序用户界面是相似的,因此我只展示了存款的用户界面,如图15-5所示。你可再次使用按键输入金额然后点击一个命令键:D键来进行存款或取款,或者E键来取消该交易。
图15-5 WorkflowATM的存款交易用户界面
假如交易成功,你会看到如图15-6所示的界面。假如失败,你将看到如图15-7所示的错误屏幕。这都没关系,点击C键重新开始该工作流。
图15-6 WorkflowATM交易成功的用户界面
图15-7 WorkflowATM交易失败的用户界面
该应用程序需要一个数据库来完整地对WF的事务能力进行测试。因此,我创建了一个简单的数据库来保存包含有PIN和账户余额的用户帐户信息。几个存储过程也被用来和数据库进行配合。所有涉及数据库更新的存储过程都必须在一个事务中执行:我要对@@trancount检查,假如它为0,我就从该存储过程中返回一个错误。假如我错误地使用一些ADO.NET代码来初始化我自己的SQL Server事务的话,这些就能证实环境事务正被使用。这些也意味着你需要创建一个数据库实例,但是这很容易实现,因为你在前面的章节中已经学过了怎样在SQL Server Management Studio Express中执行查询语句。实际上,我们将从这个任务起步因为我们将很快需要数据库来对应用程序进行开发和测试。
备注:之前我忘了提到,该数据库的创建脚本只创建了一个账户:11223344,PIN为1234。该应用程序允许你去改变账户以及你想使用的任何PIN值,但是你要么使用该账户(11223344)以及它的PIN(1234),要么创建你自己的账户记录,否则将不允许你去进行存取款。
创建Woodgrove ATM数据库
1.在本章源代码中你会找到“Create Woodgrove Database.sql”数据库生成脚本。找到它然后启动SQL Server Management Studio Express。
备注:当然SQL Server完全版也可以。
2.当SQL Server Management Studio Express打开后,把“Create Woodgrove Database.sql”文件拖拽到SQL Server Management Studio Express中。再打开该脚本文件后执行它。
3.该脚本会创建Woodgrove数据库和全部数据。该脚本的第五和第七行指明了数据库的目录和文件名。假如你不能在该默认目录(C:\Program Files\Microsoft SQL Server)下加载SQL Server,你可能需要修改将被生成的数据库的默认目录。你可以根据需要随意修改。在大多数情况下,你不需要作出修改。点击“执行”按钮运行该脚本并生成数据库。
4.当你在使用SQL Server Management Studio Express中,假如你没有进行第6章“加载和卸载实例”中的“为持久化创建SQL Server”这一节的话,需要去安装工作流持久化数据库,现在就这样做。
在完成了这四个步骤的话,你将有两个数据库:Woodgrove数据库用来保存银行业务信息,WorkflowStore数据库用来进行工作流的持久化:现在我们就来写一些工作流事务代码。
添加XA类型事务到工作流中
1.下载本章源代码,在WorkflowATM目录中你会找到WorkflowATM应用程序(WorkflowATM Completed目录中是本解决方案的最终完成版),打开WorkflowATM目录中的解决方案。你可能需要在App.Config文件中修改SQL Server的连接字符串。
2.为了让自定义活动在Visual Studio工具箱中显示出来,需要编译整个解决方案。
3.尽管WorkflowATM应用程序比较复杂,但它遵循的模式我们贯穿本书都在使用。该Windows Forms应用程序自身和工作流的通信通过一个本地通信服务实现,它使用了我用wca.exe创建的一些自定义活动。该服务在BankingServer项目中,但是该工作流却在BankingFlow项目中。我们只关注工作流自身的代码。在BankingFlow项目中找到Workflow1.cs文件,然后在工作流视图设计器中打开它准备进行编辑。该工作流会显示出你在这里看到的界面。它看起来和图15-1有些相像吗?
4.为插入XA类型的事务,首先双击DepositState活动中的CmdPressed4 EventDriven活动。
5.仔细看看左边,你会看到名称为makeDeposit1的Code活动。从工具箱中拖拽一个TransactionScope活动到makeDeposit1活动和该Code活动上面的ifElseBranchActivity11标题之间。
6.拖拽你刚才插入的该transaction scope活动下面的makeDeposit1活动,把它放到该transaction scope活动中以便让makeDeposit1 Code活动在事务中执行。
备注:随时检查MakeDeposit方法中包含的代码,它被绑定到makeDeposit1活动。你会发现这些代码有通常的ADO.NET数据库访问代码。一个有趣的事情是你可看到在该代码中没有发起SQL Server事务。相反,当该代码被执行时将使用环境事务。
7.编译整个解决方案。
8.按下F5或者从Visual Studio的“调试”菜单中选择“启动调试”去测试该应用程序。该账户应该已经设置好了。点击B键进入密码验证界面,然后键入1234(PIN码)。点击C键验证该PIN码并进入业务选择界面。
备注:假如该应用程序验证PIN失败,并且你键入的是正确的PIN码,则可能是因为Woodgrove数据库的连接字符串不正确。(我进行了错误处理是为了让应用程序不会崩溃。)验证连接字符串是正确的后再一次运行该应用程序。第5章有一些针对创建连接字符串的建议。
9.因为你为存款逻辑添加了事务,因此点击C键进行一次存款。
10.输入10去存入$100($10.00的10倍),然后点击D键去启动该业务。这个业务应该成功并且界面现在会指出该业务已成功完成。因为Woodgrove数据库创建脚本加载了一个有$1234.56的虚拟银行账户,因此现在显示的余额为$1334.56。注意你能从应用程序的左下角看到该余额。点击C键回到初始界面。
11.现在我们来强制让该业务执行失败。Deposit存储过程带有一个会引起该存储过程返回一个错误的参数值。选择“Fore Transactional error”多选框会为Deposit(存储过程)指定一个产生错误的值。因此点击B键再一次进入PIN验证界面,然后输入1234,点击C键进入银行业务选择界面。
12.再一次点击C键进行存款,然后输入10再去存入$100,但是这次在点击D键之前选中“Fore Transactional error”多选框。
13.点击D键后,应用程序会显示业务执行失败,但要注意余额。它显示当前余额仍然是$1334.56,这是该事务执行前的余额。成功的事务(步骤10)和失败的事务(步骤12)两者都由TransactionScope活动处理,它是在你第5步放进工作流中的。
这非常强大!通过包括一个单一的WF活动,我们获得了在数据库更新之上的自动的事务(XA-style)控制能力。执行一个补偿事务也能一样容易吗?碰巧,它需要更多的工作,但是把一个补偿事务添加到你的工作流中仍然不难。
向你的工作流中添加补偿事务
1.打开WorkflowATM解决方案,在工作流视图设计器中再次打开Workflow1.cs文件。找到WithdrawState活动,然后双击CmdPress5活动。这会打开CmdPressed5活动进行编辑,一旦它被打开后,你会在工作流的左边看到makeWithdrawal1 Code活动。
2.和你之前处理事务的工作类似,从Visual Studio的工具箱中拖拽一个CompensatableTransactionScope活动,把它放到makeWithdrawal1活动和它上面的ifElseBranchActivity13标题的中间。
3.从compensatableTransactionScope1活动的下面拖拽makeWithdrawal1 Code活动,把它放进事务的范围(transaction scope)之内。MakeWithdrawal方法被绑定到makeWithdrawal1活动,现在它将在一个环境事务中执行它的ADO.NET代码,就像存款(deposit)活动做的一样。
4.但是,和存款功能不同,你必须提供补偿逻辑。传统意义上业务不能回退。相反,你需要访问compensatable TransactionScope1补偿处理程序并添加你自己的补偿功能。为此,把鼠标移到compensatable TransactonScope1的标题下面的智能标签上,一旦点击它则在它下面将弹出和该活动相关的视图菜单。
5.点击最下面的“查看补偿处理程序”菜单,激活补偿处理程序视图。
6.从Visual Studio拖拽一个Code活动并把它放到该补偿处理活动中。
7.在该Code活动的ExecuteCode属性中输入CompensateWithdrawal。Visual Studio会在你的源代码中自动插入该方法并为你切换到代码编辑器界面下。
8.在为你刚刚插入的CompensateWithdrawal方法中添加下面的代码:
9.把补偿代码添加到你的工作流中后,回到工作流视图设计器上来。遵循你刚刚插入Code活动的步骤,拖拽一个自定义的Failed活动到补偿处理程序中。注意当你回到工作流视图设计器上的时候,Visual Studio可能会重新进行调整并把你带回到顶级状态活动布局界面上来。假如这样的话,可简单地在WithdrawState中再一次双击CmdPressed5活动来访问compensatableTransactionScope1活动,并再一次从它的智能标签中选择补偿处理程序视图。
10.在Failed活动的error属性中输入“Unable to withdraw funds”。
11.在刚才你插入进你工作流的Failed活动的下面,拖入一个SetState活动。在它的TargetStateName属性中选择CompletedState。
12.按下F5或者选择“调试”菜单中的“启动调试”来再次测试该应用程序。在该应用程序开始执行后,点击B键进入PIN验证界面,然后输入1234(PIN码)。点击C键对PIN进行验证并进入业务选择界面。
13.点击D键进行取款。
14.输入10取$100($10.00的十倍),然后点击D键开始交易。假如没有你的干预的话,该业务应该会成功完成,并且屏幕现在会告诉你业务完成了,账户余额是$1234.56。
15.现在我们再次让该业务执行失败。点击C键重新启动ATM,然后点击B键再次进入PIN验证界面。输入1234,然后点击C键进入银行业务选择界面。
16.输入10再次取出$100,并且选中“Fore Transactional error”复选框。然后点击D键启动该业务。
17.在你点击D键后,应用程序会指出业务执行失败并显示当前的账户余额($1234.56)。由于在MakeWithdrawal方法中没有catch语句,因此我们知道进行了取款。(假如不是如此的话,应用程序会由于一个重大的错误而被终止。)这意味着该账户实际上是取出了$100,并且补偿功能也执行了,它为该账户又添加回$100。
注意:也有其它办法看到账户的取款和存款。你可以在补偿功能模块中设置一个断点,或者假如你熟悉SQL Server Profiler并且你使用的是完整零售版本的SQL Server话,你甚至可以执行SQL Server Profiler进行查看。
本章源代码:里面包含本章的练习项目和完整代码
下一篇:WF从入门到精通(第十六章):声明式工作流