传奇模式变得容易
#go #java #distributedsystems #designpatterns

这是系列的第2部分。要阅读有关补偿动作的信息,请查看Compensating Actions, Part of a Complete Breakfast with Sagas

与萨加斯的旅行计划,但没有行李

所以您的家人上次去当地公园时,每个人都在谈论传奇设计模式,现在您想知道它是什么它。众所周知,软件设计与时尚 1 趋势 2

萨加斯的情况

如果您想知道传奇模式是否适合您的情况部分执行是不良的 吗?事实证明,这正是传说有用的地方。也许您正在检查库存,收取用户的信用卡,然后履行订单。也许您正在管理供应链。传奇模式很有帮助,因为它基本上可以充当状态机器存储程序的进度,防止多个信用卡费用,如有必要,并确切地知道如何在功率损失的情况下安全地以一致的状态恢复。

一个常见的基于生命的示例,用于解释传奇模式补偿失败的方式是行程计划。假设您渴望吸收西雅图杜瓦米什(Duwamish)领土的雨水。您需要购买飞机票,预留酒店,并获得雷尼尔山(Mount Rainier)背包旅行体验的票。所有这三个任务都是耦合的:如果您无法购买机票,则没有理由获得其余的。如果您有一张机票,但无处可住,您将要取消该飞机预订(或重试酒店预订或找到其他地方)。最后,如果您可以预订背包旅行,那么really no other reason可以来到西雅图,这样您就可以取消整个事情。 (开玩笑!)

Saga Pattern Diagram Example

上面:面对旅行计划失败的简单模型。

有很多事情要做,或者不打扰现实世界中的软件应用程序:如果您成功向用户向用户收取物品的费用,但是您的履行服务报告说,该物品已经掉了库存,如果您不退款,您会让用户不高兴。如果您有相反的问题并免费提供物品,那么您将倒闭。如果机器协调机器学习数据处理管道崩溃,但随行机器随处可在处理数据以将其数据报告给您的数据中,您可能会在手上有一个非常昂贵的计算资源费用 3 < /sup>。在所有这些情况下正是传奇模式提供的。在传奇的言论中,所有这些任务都称为长期交易。这不一定意味着这样的动作长期运行,只是它们在逻辑时间 4 要比与本地交互数据库。

您如何构建传奇?

一个由两个部分组成:

  1. 如果您需要撤消某些东西(即compensations),则定义了行为。
  2. 努力朝着前进进步的行为(即保存状态以知道面对失败时从哪里恢复)

这个博客的狂热读者会记得我最近写了一个post about compensating actions。从上面可以看到,薪酬仅是传奇设计模式的一半。另一半所暗示的上面是整个系统的国家管理。如果单个步骤(或时间术语中, Activity )失败,则可以帮助您知道如何恢复。但是,如果整个系统降低了怎么办?您从哪里开始备份?由于并非每个步骤都可能附带薪酬,因此您被迫根据存储的薪酬来做自己的最佳猜测。传奇模式跟踪您目前的位置,以便您可以继续前进。

那么如何在自己的代码中实现sagas?

我很高兴你问。

向前倾斜

耳语中的耳语

这是一个问题的问题,因为通过时间运行代码,您自动 在任何级别上都保存状态并在故障上重新恢复。这意味着带有时间的传奇模式就像在步骤( Activity )失败时要进行的补偿一样简单。结尾。

_为什么_为什么这个魔术是暂时的,通过设计,它会自动跟踪程序的进度,并且可以在面对灾难性失败的情况下接收到它的位置。此外,暂时性将在失败上重试活动,而无需添加除指定重试策略之外的任何代码,例如:

RetryOptions retryoptions = RetryOptions.newBuilder()
       .setInitialInterval(Duration.ofSeconds(1))
       .setMaximumInterval(Duration.ofSeconds(100))
       .setBackoffCoefficient(2)
       .setMaximumAttempts(500).build();

要进一步了解这种自动化的工作原理,请继续关注我即将发表的编舞和编排的帖子,这是实施Sagas的两种常见方法。

因此,要使用度假预订步骤加上我希望失败的薪酬来表达我的程序的高级逻辑,在伪代码中看起来像以下内容:

try:
   registerCompensationInCaseOfFailure(cancelHotel)
   bookHotel
   registerCompensationInCaseOfFailure(cancelFlight)
   bookFlight
   registerCompensationInCaseOfFailure(cancelExcursion)
   bookExcursion
catch:
   run all compensation activities

在Java中,Saga类跟踪您的补偿:

@Override
public void bookVacation(BookingInfo info) {
   Saga saga = new Saga(new Saga.Options.Builder().build());
   try {
       saga.addCompensation(activities::cancelHotel, info.getClientId());
       activities.bookHotel(info);

       saga.addCompensation(activities::cancelFlight, info.getClientId());
       activities.bookFlight(info);

       saga.addCompensation(activities::cancelExcursion, 
                            info.getClientId());
       activities.bookExcursion(info);
   } catch (TemporalFailure e) {
       saga.compensate();
       throw e;
   }
}

在其他语言SDK中,您可以轻松地编写addCompensation,而compensate则可以自己函数。这是GO中的版本:

func (s *Compensations) AddCompensation(activity any, parameters ...any) {
    s.compensations = append(s.compensations, activity)
    s.arguments = append(s.arguments, parameters)
}

func (s Compensations) Compensate(ctx workflow.Context, inParallel bool) {
    if !inParallel {
        // Compensate in Last-In-First-Out order, to undo in the reverse order that activies were applied.
        for i := len(s.compensations) - 1; i >= 0; i-- {
            errCompensation := workflow.ExecuteActivity(ctx, s.compensations[i], s.arguments[i]...).Get(ctx, nil)
            if errCompensation != nil {
                workflow.GetLogger(ctx).Error("Executing compensation failed", "Error", errCompensation)
            }
        }
    } else {
        selector := workflow.NewSelector(ctx)
        for i := 0; i < len(s.compensations); i++ {
            execution := workflow.ExecuteActivity(ctx, s.compensations[i], s.arguments[i]...)
            selector.AddFuture(execution, func(f workflow.Future) {
                if errCompensation := f.Get(ctx, nil); errCompensation != nil {
                    workflow.GetLogger(ctx).Error("Executing compensation failed", "Error", errCompensation)
                }
            })
        }
        for range s.compensations {
            selector.Select(ctx)
        }
    }
}

高级步骤和薪酬代码看起来与Java版本非常相似:

func TripPlanningWorkflow(ctx workflow.Context, info BookingInfo) (err error) {
   options := workflow.ActivityOptions{
       StartToCloseTimeout: time.Second * 5,
       RetryPolicy:         &temporal.RetryPolicy{MaximumAttempts: 2},
   }

   ctx = workflow.WithActivityOptions(ctx, options)

   var compensations Compensations

   defer func() {
       if err != nil {
           // activity failed, and workflow context is canceled
           disconnectedCtx, _ := workflow.NewDisconnectedContext(ctx)
           compensations.Compensate(disconnectedCtx, true)
       }
   }()

   compensations.AddCompensation(CancelHotel)
   err = workflow.ExecuteActivity(ctx, BookHotel, info).Get(ctx, nil)
   if err != nil {
       return err
   }

   compensations.AddCompensation(CancelFlight)
   err = workflow.ExecuteActivity(ctx, BookFlight, info).Get(ctx, nil)
   if err != nil {
       return err
   }

   compensations.AddCompensation(CancelExcursion)
   err = workflow.ExecuteActivity(ctx, BookExcursion, info).Get(ctx, nil)
   if err != nil {
       return err
   }

   return err
}

上面的高级代码序列称为临时 Workflow ,如前所述,通过暂时运行,我们不必担心实施任何簿记以通过事件采购或添加重试和重新启动逻辑来跟踪我们的进度,因为这一切都来了自由的。因此,当编写具有暂时性的代码时,您只需要担心编写补偿,其余的免费提供。

势力

好吧,好吧,要担心的是第二件事。第二部分努力朝着前进的进步涉及在面对失败时重试活动。让我们深入研究其中一个步骤,对吗? Permulal会完成重试和跟踪您的整体进度的所有繁重提升,但是,由于可以重述代码,因此您,程序员需要确保每个时间活动都是 didempotent 。这意味着bookFlight的观察到的结果是相同的,无论是一次还是多次。为了使其更具体,可以设置某些字段foo=3的功能是智力的,因为后来foo将是3个函数,无论您称之为多少次。函数foo += 3不符合掌握,因为foo的值取决于调用函数的次数。非数字有时会看起来更微妙:如果您有一个允许重复记录的数据库,则调用INSERT INTO foo (bar) VALUES (3)的函数将在表中随着时间的流逝而在表中创建尽可能多的记录,因此并不是掌声。默认情况下,发送电子邮件或转移资金的功能的幼稚实现也不是不属于的。

,如果您现在缓慢退缩,因为您的现实世界应用比设置foo=3更复杂的事情,请振作起来。有一个解决方案。您可以使用不同的标识符,称为 iDempotency键,有时称为referenceId或类似于唯一标识特定交易的东西,并确保酒店预订交易有效一次。可以根据您的应用程序需求来定义此IDEMTENCY密钥的方式。在旅行计划应用程序中,clientIdBookingInfo中的一个字段用于唯一识别交易。

type BookingInfo struct {
   Name     string
   ClientId string
   Address  string
   CcInfo   CreditCardInfo
   Start    date.Date
   End      date.Date
}

您可能还看到了用于在上述Java工作流程中注册补偿的clientId

saga.addCompensation(activities::cancelHotel, info.getClientId());

但是,使用clientId作为我们的钥匙限制了特定人一次预订一个以上的假期。这可能是我们想要的。但是,某些业务应用程序可能会选择通过组合clientIdworkflowId来构建一个势力密钥,以一次每客户预订一个以上的假期。如果您想要一个真正独特的势力键,则可以将UUID传递到工作流程中。根据您的应用程序的需求,选择取决于您。

Many third-party APIs that handle money为此目的已经接受了势力键。如果您需要自己实施这样的操作,请使用原子质写作来保留到目前为止看到的势力键的记录,并且如果已经看到它的势力键,则不执行操作。设置。

受益与复杂性

传奇模式确实为您的代码增加了复杂性,因此,重要的是不要在代码仅在中实现它,因为您有微服务。但是,如果您需要完成涉及多种服务和部分执行的任务(例如,使用机票和酒店预订旅行)实际上并不是成功的,那么传奇将成为您的朋友。此外,如果您发现自己的传教士变得尤其是笨拙,那么可能是时候重新考虑如何将微服务分配,然后卷起袖子进行重构。总体而言,暂时性使代码中实现传奇模式相对较小,因为您只需要每个步骤都需要write the compensations。请继续关注我的下一篇文章,在那里我深入研究萨加斯和订阅场景,暂时的尤其是降低与Sagas合作时的复杂性。

使用本文中提到的代码的完整存储库可以找到on GitHub

如果您想使用Perimal查看Sagas的其他教程,请查看以下资源:

我的一位同事Dominik Tornow在YouTube上给了Sagas的简介。

在我们的coursestutorialsdocsvideos中了解有关时间的更多信息。

笔记


  1. 显然,不要重新设计您的系统,只是因为它是新的热度。除非是一个新的JavaScript框架。然后npm install急救的新包装。 ð

  2. 不用担心,萨加斯的趋势;他们在数据库中遇到了since the 80s。您可以让您感到舒适,知道您的项目具有经典的设计。

  3. 并不是说作者在这种情况下绝对有任何经验。 咳嗽新车的价格

  4. 逻辑时间是分布式计算中的一个概念,以描述分布式计算中不同机器上发生的事件的时机,因为机器可能没有物理同步全局时钟。逻辑时机只是这些机器上发生的事件的因果序。在长期运行的交易中,基本上归结为在不同机器上进行了许多步骤。