分布式系统解耦模式:用事件代表时间触发Cron计划任务

542 阅读5分钟

计划任务一般都喜欢使用Cron作业来完成,比如使用spring scheduler或Quartz,本模式推荐使用黑盒式的不可知事件替代Cron作业。

问题

许多业务流程涉及需要在将来执行的某些操作或工作或工作负载。它可以是一次性动作或重复动作,可以安排在特定日期(例如圣诞节),重复日期(月的最后一个工作日)或超时(从现在起30天)。

我们希望保持整个流程的领域逻辑在单个(微)服务中很好地隔离,因此这也是此操作的逻辑所在。但是有一小部分逻辑不在这项服务中:就是事实执行需要进行,以及何时需要进行。

Cron是最麻烦的:它只适用于重复操作,并且外部工具几乎不能控制。在大型系统中,cron文件可能造成巨大的混乱。更现代的解决方案允许我们通过在代码中调用API来安排使用,这样可将逻辑移动到我们自己的服务中。但从根本上说,调度程序仍然是一个单独的服务,但是它涉及到知识比它应该知道的会更多。使用DDD术语,我们业务流程的无所不在的语言在此服务中泄漏。我们的系统中必须有更多类型的元素和更多的可移动部件。

解决

思维转换是:将时间流逝视为另一个领域事件,就像所有其他事件一样。毕竟,如果我们将领域事件定义为与业务相关的事件的粒度时间点,比如下一个工作日,月或季度。

在新的设计中,cron或调度程序会定期发出普通的时间段事件,例如DayHasPassed {date}午夜事件或一个 QuarterHasPassed {year, quarter}。所有感兴趣的服务都会收听此事件。他们可以通过执行操作,增加计数器,或通过查询某个数据库并按日期过滤来对其做出反应,以查找有一些工作要做的项目。

举例

时间管理着一切,有用的示例:可计费小时,订阅,资源使用,租赁,累积利息,付款,报告,工资,维护计划以及所有循环。

在发票到期日,我们需要向客户发送提醒。在旧设计中,我们可以设置一个调用的cron作业CheckForOverdueInvoices;在新设计中,cron DayHasPassed每隔午夜就产生一次,在InvoiceDebtCollection中监听DayHasPassed。

每当此事件到达时,InvoiceDebtCollection查询SELECT * FROM Invoice AS i WHERE DATEDIFF(i.dueDate, NOW()) >= 30。它可以直接发送提醒,但更好的方法是发出新InvoiceBecameOverdue事件。

现在,该服务可以收听自己的事件并发送提醒。其他服务也可以对InvoiceBecameOverdue 做出反应,例如调整收入预测或暂停帐户。

时间流逝事件也可以使用在更具特定领域。纳斯达克的盘前交易时间为04:00至09:30,然后是正常交易时间至16:00,盘后交易时间为20:00。假期没有交易。服务可以为每个启动和关闭生成事件,可以由许多感兴趣的服务使用。

优点

在上面的示例中,事件日志将显示:

CustomerWasInvoiced
DayHasPassed
DayHasPassed
...
InvoiceBecameOverdue
ReminderWasSent
AccountWasSuspended

使用时间元素编写业务流程测试非常优雅:

场景: If no payment is received after 15 days, we suspend the account
Given CustomerWasInvoiced
  And DayHasPassed 
  And DayHasPassed 
  And (...) 
 When DayHasPassed
 Then InvoiceBecameOverdue
  And AccountWasSuspended 
  
场景: If payment is received in time, we do nothing
Given CustomerWasInvoiced
  And DayHasPassed 
  And DayHasPassed 
 When PaymentWasMade
 Then Nothing        

更重要的是,这是一种很好的反应方法。当服务将命令发送到另一个服务时,它需要知道该其他服务接受该命令。当我们所做的只是发送通用时间事件通道时,调度程序不需要知道谁在听,消费者应该如何反应,或者是否还有任何服务可以监听。所有决策和领域知识都归接收者所有。这是很好的脱钩。

它也是时间解耦:调度程序可以将此时间通道事件放在队列中,并且此时消费者是否可用来处理事件并不重要。尽管如此,消费者可能会停顿几天,然后只需赶上并处理DayHasPassed队列中的所有事件。

在事件采购中,您只需将DayHasPassed事件存储在事件存储中,这样您就可以完全按照时间发生的方式回放整个历史记录,而不依赖于外部源。

时间事件和其他领域事件类型使用完全一样的格式和协议,通过与其他所有内容相同的消息传递基础结构发送它 这使得该模式在实现方面非常便宜。

弱点

原来在另一个其他地方上存在的一些领域知识:用于计算何时需要发生的域逻辑,例如“每月10日”,现在已经有效地从cron或调度程序转移到服务中。在实践中,这不是什么大不了的事,因为你可以找到时间库来为你完成工作。从好的方面来说,“除了周末,满月期间或年度办公室聚会期间,”每个月的第10个月“都是调度员无论如何也不会为你做的事情。

值得注意的是,您不希望将时间段事件模式用于实时系统。我们可以轻松地实现每年365个DayHasPassed事件,但对于处理几分钟或几秒或更短时间的系统,这是不可行的。幸运的是,对于程序员而言,在大多数企业中,“立即”一词意味着“在工作日结束时”,或“在本周末”,或“在事情发生的月份之后的季度结束之前” 。