写点什么

架构误区系列 4:variable task

作者:agnostic
  • 2022-11-13
    上海
  • 本文字数:1407 字

    阅读完需:约 5 分钟

本期的架构误区系列关于一个领域建模的问题,针对的是一个常见的场景:一个单据创建后,在后续的特定时间需要触发一个任务的执行,但是如果单据修改,这个任务的执行时间点和内容就会发生变化。针对这种场景,应该如何设计这个任务触发的机制呢?


在最近的远期能力的建设中,就有这样一个场景:用户下完远期单据后,注册一个到期的兜底交割任务。如果用户提前交割,任务取消;如果用户修改了远期单据,任务需要重新注册(时间、金额等条件可能发生变化)。在 Review 架构方案时,我们发现现存的设计是这样的(和需求的语意非常的 match^_^):

  1. FWD 单据创建成功后,register 一个兜底交割 task。

  2. 如果用户提前交割,这个任务执行的时候已经没有可用金额,会自动 close。

  3. 如果用户修改单据,会取消任务,再重新注册一个新的任务。


初看上去,这个设计和需求非常符合,而且也没有太大的逻辑问题。但是,总是感觉这个设计过于复杂,不满足 KISS 原则。

同时,我们试想一种场景:由于可以由客服的同学代替客户修改单据,这个时候有审批的流程,同时可能有并行修改的可能,所以第三步的取消并重新注册这个 activity 的分支逻辑就显得比较复杂:

  • 首先需要考虑多次修改的版本问题,对于版本 A 的修改只能取消版本 A 的任务同时注册自己的任务,这里需要严格保证修改的串行化。

  • 如果有代码 bug,或者系统 bug,导致前置任务没有取消,不可避免在任务执行的过程中会出现问题,或者要考虑对版本的判断。


其实,这个设计,最大的一个问题就是设计了两个领域对象:单据和任务,同时两个领域对象之间需要严格保证状态的一致性。这种强耦合的设计,会导致实现和测试验证的复杂度。


更优雅的设计,需要解开两者的耦合,将代码的复杂度降低。这里有两种选择:

用单据无关任务来扫描

这个设计非常简洁,由于兜底交割时按天发生的,考虑到时区的因素,我们设计一个每小时执行一次的定时任务。定时任务执行的时候去扫描所有到期时间为本小时的单据,并执行兜底交割。

这个任务,直接将去除了兜底交割任务的注册,通过定时扫描类似银行计息的方式,做兜底交割,彻底杜绝了状态耦合情况的发生。

我们在设计的时候,只需要保证每小时都有任务被调度:这个可以通过监控简单的保证。


用 immutable 的任务代替可以取消的任务

还是保持注册定时交割任务的设计(下单和修改的时候都注册)。在兜底交割任务上会记录执行时间、远期单号 &版本信息。

在执行任务的时候,首先会根据单号锁定对应的远期单。然后判断远期单的版本是否交割任务的版本,如果不是,说明单据已经修改,ignore 这个任务。

如果是,再判断是否有可交割金额,如果否表示已完成交割,close 任务。如果还有交割金额,再执行交割任务。

我们来看一下异常场景:

  • 如果在注册任务后,远期单被修改了,会注册新的任务,就的任务会因为版本不同而被忽略。

  • 如果在注册任务后,远期单被全额交割了,任务执行的时候会因为没有可交割金额而不执行。

所以,我们只需要考虑这两种特殊逻辑分支即可。


总之,这两种设计各有千秋:

  • 单据无关任务的设计最简单,但是可能在扫描单据的时候,需要全库扫描,效率稍微有点低(其实状态和时间有索引,问题也还好)

  • immutable 任务的设计,逻辑上稍微复杂,注册时要记录版本号执行的时候需要判断版本是否一致。但是性能上由于只扫描任务表,稍有优势。

但不管是那种设计,都杜绝了注册/取消这个复杂的逻辑,在注册任务的时候也解除了和单据的耦合关系。基本上遵循了 KISS 的原则。


注:KISS - Keep It Simple & Stupid

发布于: 刚刚阅读数: 3
用户头像

agnostic

关注

还未添加个人签名 2019-02-14 加入

还未添加个人简介

评论

发布
暂无评论
架构误区系列4:variable task_延迟任务_agnostic_InfoQ写作社区