Привет всем энтузиастам-разработчикам! Сегодня окунемся в увлекательную область программной разработки, где шаблоны подобны путеводным звездам на бескрайних просторах кода, а конкретно шаблон Saga  —  это наш компас на пути создания надежных, масштабируемых систем Go.

Этим шаблоном  —  убийцей распределенной транзакции  —  каждой транзакции на всех этапах бизнес-процесса предоставляется соответствующая транзакция компенсации. Так что, когда появляется условие ошибки, из-за которого продолжение бизнес-процесса становится невозможным, эти транзакции компенсации выполняются для уже завершенных этапов. Проделанная к этому моменту работа фактически отменяется, чем обеспечивается согласованность системы.

Управление продолжительными транзакциями

Шаблон Saga  —  важный инструмент для управления продолжительными транзакциями в распределенных системах. Сложные транзакции разбиваются им на маленькие, управляемые этапы, так обеспечиваются согласованность и надежность данных.

В шаблоне Saga каждый этап транзакции сопровождается действием компенсации, необходимым для корректного восстановления в случае сбоя. Так система остается отказоустойчивой и согласованной.

Когда применяется Saga?

Шаблон приходится кстати в сценариях управления длительными или распределенными транзакциями в службах или компонентах микросервисной архитектуры, сложными бизнес-процессами, транзакциями компенсации, а в конечном итоге согласованностью.

Вот реальные примеры таких сценариев.

При онлайн-покупке системе нужно зарезервировать товар, обработать платеж, отправить товар. Если какой-либо этап не пройден, то предыдущие системой отменяются для поддержания согласованности.

При бронировании перелета или гостиницы системе нужно подтвердить бронь, обработать платеж, оформить билеты. Если какой-либо этап не пройден, предыдущие системой отменяются во избежание двойного бронирования или некорректных начислений.

При доставке еды системой принимается заказ, готовится еда, назначается водитель доставки, обрабатывается платеж. Если какой-либо этап не пройден, предыдущие системой отменяются во избежание некорректной обработки заказа.

При финансовых транзакциях, денежных переводах или онлайн-заявках на кредит системой подтверждается запрос, обрабатывается транзакция, обновляется счет. Если какой-либо этап не пройден, предыдущие системой откатываются во избежание некорректных транзакций или несоответствия данных.

Переходим сразу к коду

В этом примере сделаем акцент на двух сценариях обработки: заказа и оплаты. В шаблоне Saga функции MakeOrder и CancelOrder выполняются как локальные транзакции. Если при заказе проблем нет, выполняется функция ProcessPayment, то есть этап оплаты. Если проблема на этапе оплаты, выполняется функция CancelPayment  —  это действие компенсации.

type Order struct {
Id uint
Product string
Quantity int
Total float64
}

func ProcessOrder(workflow *wf.Workflow) {

order := &Order{
Id: 123,
Product: "Laptop",
Quantity: 1,
Total: 1000.00,
}

workflow.AddStep("make_order", wf.SagaStep{Transaction: order.MakeOrder, Compensate: order.CancelOrder})
workflow.AddStep("process_payment", wf.SagaStep{Transaction: order.ProcessPayment, Compensate: order.CancelPayment})
}

func (o *Order) MakeOrder() error {
log.Printf("Order created: %v\n", o)
return nil
}

func (o *Order) CancelOrder() error {
log.Printf("Order cancelled: %d\n", o.Id)
return nil
}

func (o *Order) ProcessPayment() error {
return errors.New("payment failed")
}

func (o *Order) CancelPayment() error {
log.Printf("Payment cancelled: %v\n", o.Total)
return nil
}

В этом коде обработки заказа намеренно спровоцированы ошибки функции ProcessPayment, так продемонстрируем откат всех предыдущих транзакций.

Вот код рабочих процессов:

type LocalTransaction func() error
type CompensatingAction func() error

type Workflow struct {
Steps map[string]SagaStep
}

type SagaStep struct {
Transaction LocalTransaction
Compensate CompensatingAction
}

func NewWorkflow() *Workflow {
return &Workflow{
Steps: make(map[string]SagaStep),
}
}

func (w *Workflow) AddStep(name string, step SagaStep) {
w.Steps[name] = step
}

func (w *Workflow) Execute() error {
for stepName, stepFunc := range w.Steps {
log.Printf("[Executing step] %s", stepName)

if err := stepFunc.Transaction(); err != nil {
log.Printf("[Error executing step] %s: %s", stepName, err)

for n, f := range w.Steps {
log.Printf("[Compensating step] %s", n)
f.Compensate()

if stepName == n {
break
}
}
}
}

return nil
}
package main

import (
order "go-saga-workflow/internal"
wf "go-saga-workflow/pkg"
"log"
)

func main() {
workflow := wf.NewWorkflow()
order.ProcessOrder(workflow)

err := workflow.Execute()

if err != nil {
log.Printf("saga execution failed: %+v\n", err)
} else {
log.Printf("saga executed successfully\n")
}
}

Подводя итоги

Шаблон Saga  —  важный инструмент в арсенале разработчика. Эффективно используя его возможности, разработчики создают отказоустойчивые, масштабируемые, легко сопровождаемые системы. Благодаря Saga транзакции, которыми охватывается несколько служб или компонентов, становятся более управляемыми, этим обеспечиваются согласованность и надежность данных даже в случае сбоев.

Поэтому, когда в следующий раз встретите сценарий со сложными транзакциями из области программной разработки, вспомните о шаблоне Saga  —  надежном инструменте для разбора нюансов распределенных систем.

В этом простом примере шаблон Saga на Go описан посредством моделирования этапов заказа и оплаты. Можете расширить его, усложнив рабочие процессы и применяя различные шаблоны. Подробнее код проекта  —  на GitHub.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Cem Bideci: Implementing the Saga Pattern in Go: A Hands-On Approach

Предыдущая статьяСекреты в Android. Часть 2