架构误区系列 4:variable task
本期的架构误区系列关于一个领域建模的问题,针对的是一个常见的场景:一个单据创建后,在后续的特定时间需要触发一个任务的执行,但是如果单据修改,这个任务的执行时间点和内容就会发生变化。针对这种场景,应该如何设计这个任务触发的机制呢?
在最近的远期能力的建设中,就有这样一个场景:用户下完远期单据后,注册一个到期的兜底交割任务。如果用户提前交割,任务取消;如果用户修改了远期单据,任务需要重新注册(时间、金额等条件可能发生变化)。在 Review 架构方案时,我们发现现存的设计是这样的(和需求的语意非常的 match^_^):
FWD 单据创建成功后,register 一个兜底交割 task。
如果用户提前交割,这个任务执行的时候已经没有可用金额,会自动 close。
如果用户修改单据,会取消任务,再重新注册一个新的任务。
初看上去,这个设计和需求非常符合,而且也没有太大的逻辑问题。但是,总是感觉这个设计过于复杂,不满足 KISS 原则。
同时,我们试想一种场景:由于可以由客服的同学代替客户修改单据,这个时候有审批的流程,同时可能有并行修改的可能,所以第三步的取消并重新注册这个 activity 的分支逻辑就显得比较复杂:
首先需要考虑多次修改的版本问题,对于版本 A 的修改只能取消版本 A 的任务同时注册自己的任务,这里需要严格保证修改的串行化。
如果有代码 bug,或者系统 bug,导致前置任务没有取消,不可避免在任务执行的过程中会出现问题,或者要考虑对版本的判断。
其实,这个设计,最大的一个问题就是设计了两个领域对象:单据和任务,同时两个领域对象之间需要严格保证状态的一致性。这种强耦合的设计,会导致实现和测试验证的复杂度。
更优雅的设计,需要解开两者的耦合,将代码的复杂度降低。这里有两种选择:
用单据无关任务来扫描
这个设计非常简洁,由于兜底交割时按天发生的,考虑到时区的因素,我们设计一个每小时执行一次的定时任务。定时任务执行的时候去扫描所有到期时间为本小时的单据,并执行兜底交割。
这个任务,直接将去除了兜底交割任务的注册,通过定时扫描类似银行计息的方式,做兜底交割,彻底杜绝了状态耦合情况的发生。
我们在设计的时候,只需要保证每小时都有任务被调度:这个可以通过监控简单的保证。
用 immutable 的任务代替可以取消的任务
还是保持注册定时交割任务的设计(下单和修改的时候都注册)。在兜底交割任务上会记录执行时间、远期单号 &版本信息。
在执行任务的时候,首先会根据单号锁定对应的远期单。然后判断远期单的版本是否交割任务的版本,如果不是,说明单据已经修改,ignore 这个任务。
如果是,再判断是否有可交割金额,如果否表示已完成交割,close 任务。如果还有交割金额,再执行交割任务。
我们来看一下异常场景:
如果在注册任务后,远期单被修改了,会注册新的任务,就的任务会因为版本不同而被忽略。
如果在注册任务后,远期单被全额交割了,任务执行的时候会因为没有可交割金额而不执行。
所以,我们只需要考虑这两种特殊逻辑分支即可。
总之,这两种设计各有千秋:
单据无关任务的设计最简单,但是可能在扫描单据的时候,需要全库扫描,效率稍微有点低(其实状态和时间有索引,问题也还好)
immutable 任务的设计,逻辑上稍微复杂,注册时要记录版本号执行的时候需要判断版本是否一致。但是性能上由于只扫描任务表,稍有优势。
但不管是那种设计,都杜绝了注册/取消这个复杂的逻辑,在注册任务的时候也解除了和单据的耦合关系。基本上遵循了 KISS 的原则。
注:KISS - Keep It Simple & Stupid
版权声明: 本文为 InfoQ 作者【agnostic】的原创文章。
原文链接:【http://xie.infoq.cn/article/c55ec8e43c564c1ef0f6dfb9c】。文章转载请联系作者。
评论