Pola Saga dalam Transaksi Terdistribusi - Dengan Contoh dalam Go

Transaksi dalam Microservices dengan Pola Saga

Konten Halaman

Pola Saga
menyediakan solusi yang elegan dengan memecah transaksi terdistribusi menjadi serangkaian transaksi lokal dengan tindakan kompensasi.

Alih-alih bergantung pada kunci terdistribusi yang dapat menghalangi operasi di seluruh layanan, Saga memungkinkan konsistensi akhir melalui urutan langkah-langkah yang dapat dibatalkan, menjadikannya ideal untuk proses bisnis yang berlangsung lama.

Dalam arsitektur mikroservis, menjaga konsistensi data di seluruh layanan adalah salah satu tantangan terbesar. Transaksi ACID tradisional tidak berfungsi ketika operasi mencakup beberapa layanan dengan basis data independen, meninggalkan pengembang mencari pendekatan alternatif untuk memastikan integritas data.

Panduan ini menunjukkan implementasi Pola Saga dalam Go dengan contoh praktis yang mencakup pendekatan orkestrasi dan koreografi. Jika Anda membutuhkan referensi cepat untuk dasar-dasar Go, Go Cheat Sheet menyediakan gambaran yang membantu.

pekerja konstruksi dengan transaksi terdistribusi
Gambar yang menarik ini dihasilkan oleh Model AI Flux 1 dev.

Memahami Pola Saga

Pola Saga awalnya dijelaskan oleh Hector Garcia-Molina dan Kenneth Salem pada tahun 1987. Dalam konteks mikroservis, ini adalah urutan transaksi lokal di mana setiap transaksi memperbarui data dalam satu layanan. Jika langkah mana pun gagal, transaksi kompensasi dieksekusi untuk membatalkan efek dari langkah-langkah sebelumnya.

Berbeda dengan transaksi terdistribusi tradisional yang menggunakan komit dua fase (2PC), Saga tidak memegang kunci di seluruh layanan, menjadikannya cocok untuk proses bisnis yang berlangsung lama. Pertukaran yang terjadi adalah konsistensi akhir daripada konsistensi kuat.

Karakteristik Utama

  • Tidak Ada Kunci Terdistribusi: Setiap layanan mengelola transaksi lokalnya sendiri
  • Tindakan Kompensasi: Setiap operasi memiliki mekanisme rollback yang sesuai
  • Konsistensi Akhir: Sistem akhirnya mencapai keadaan yang konsisten
  • Berlangsung Lama: Cocok untuk proses yang memakan waktu beberapa detik, menit, atau bahkan jam

Pendekatan Implementasi Saga

Ada dua pendekatan utama untuk mengimplementasikan Pola Saga: orkestrasi dan koreografi.

Pola Orkestrasi

Dalam orkestrasi, seorang koordinator pusat (orkestrator) mengelola seluruh alur transaksi. Orkestrator bertanggung jawab untuk:

  • Memanggil layanan dalam urutan yang benar
  • Mengelola kegagalan dan memicu kompensasi
  • Memelihara status saga
  • Mengkoordinasikan ulang coba dan timeout

Keuntungan:

  • Kontrol dan visibilitas terpusat
  • Lebih mudah dipahami dan di-debug
  • Penanganan kesalahan dan pemulihan yang lebih baik
  • Pengujian alur keseluruhan yang lebih sederhana

Kekurangan:

  • Titik kegagalan tunggal (meskipun ini dapat diminimalkan)
  • Layanan tambahan yang harus dipertahankan
  • Dapat menjadi bottleneck untuk alur yang kompleks

Contoh dalam Go:

type OrderSagaOrchestrator struct {
    orderService    OrderService
    paymentService  PaymentService
    inventoryService InventoryService
    shippingService ShippingService
}

func (o *OrderSagaOrchestrator) CreateOrder(order Order) error {
    sagaID := generateSagaID()
    
    // Langkah 1: Membuat pesanan
    orderID, err := o.orderService.Create(order)
    if err != nil {
        return err
    }
    
    // Langkah 2: Menahan persediaan
    if err := o.inventoryService.Reserve(order.Items); err != nil {
        o.orderService.Cancel(orderID) // Kompensasi
        return err
    }
    
    // Langkah 3: Memproses pembayaran
    paymentID, err := o.paymentService.Charge(order.CustomerID, order.Total)
    if err != nil {
        o.inventoryService.Release(order.Items) // Kompensasi
        o.orderService.Cancel(orderID)          // Kompensasi
        return err
    }
    
    // Langkah 4: Membuat pengiriman
    if err := o.shippingService.CreateShipment(orderID); err != nil {
        o.paymentService.Refund(paymentID)      // Kompensasi
        o.inventoryService.Release(order.Items) // Kompensasi
        o.orderService.Cancel(orderID)          // Kompensasi
        return err
    }
    
    return nil
}

Pola Koreografi

Dalam koreografi, tidak ada koordinator pusat. Setiap layanan mengetahui apa yang harus dilakukan dan berkomunikasi melalui acara. Layanan mendengarkan acara dan bereaksi sesuai. Pendekatan berbasis acara ini sangat kuat ketika dikombinasikan dengan platform streaming pesan seperti AWS Kinesis, yang menyediakan infrastruktur yang skalabel untuk distribusi acara di seluruh mikroservis. Untuk panduan menyeluruh tentang implementasi mikroservis berbasis acara dengan Kinesis, lihat Membangun Mikroservis Berbasis Acara dengan AWS Kinesis.

Keuntungan:

  • Terdesentralisasi dan skalabel
  • Tidak ada titik kegagalan tunggal
  • Layanan tetap terpisah
  • Cocok alami untuk arsitektur berbasis acara

Kekurangan:

  • Lebih sulit memahami alur keseluruhan
  • Sulit di-debug dan dilacak
  • Penanganan kesalahan yang kompleks
  • Risiko ketergantungan siklik

Contoh dengan Arsitektur Berbasis Acara:

// Layanan Pesanan
type OrderService struct {
    eventBus EventBus
    repo     OrderRepository
}

func (s *OrderService) CreateOrder(order Order) (string, error) {
    orderID, err := s.repo.Save(order)
    if err != nil {
        return "", err
    }
    
    s.eventBus.Publish("OrderCreated", OrderCreatedEvent{
        OrderID:    orderID,
        CustomerID: order.CustomerID,
        Items:      order.Items,
        Total:      order.Total,
    })
    
    return orderID, nil
}

func (s *OrderService) HandlePaymentFailed(event PaymentFailedEvent) error {
    return s.repo.Cancel(event.OrderID) // Kompensasi
}

// Layanan Pembayaran
type PaymentService struct {
    eventBus EventBus
    client   PaymentClient
}

func (s *PaymentService) HandleOrderCreated(event OrderCreatedEvent) {
    paymentID, err := s.client.Charge(event.CustomerID, event.Total)
    if err != nil {
        s.eventBus.Publish("PaymentFailed", PaymentFailedEvent{
            OrderID: event.OrderID,
        })
        return
    }
    
    s.eventBus.Publish("PaymentSucceeded", PaymentSucceededEvent{
        OrderID:   event.OrderID,
        PaymentID: paymentID,
    })
}

func (s *PaymentService) HandleInventoryReservationFailed(event InventoryReservationFailedEvent) error {
    // Kompensasi: mengembalikan pembayaran
    return s.client.Refund(event.PaymentID)
}

Strategi Kompensasi

Kompensasi adalah inti dari Pola Saga. Setiap operasi harus memiliki kompensasi yang sesuai untuk membatalkan efeknya.

Jenis Kompensasi

  1. Operasi yang Dapat Dibalikkan: Operasi yang dapat dibatalkan langsung

    • Contoh: Melepaskan persediaan yang telah dipesan, mengembalikan pembayaran
  2. Tindakan Kompensasi: Operasi berbeda yang mencapai efek terbalik

    • Contoh: Membatalkan pesanan daripada menghapusnya
  3. Kompensasi Pessimis: Alokasi sumber daya yang dapat dilepaskan

    • Contoh: Menahan persediaan sebelum membebankan pembayaran
  4. Kompensasi Optimis: Eksekusi operasi dan kompensasi jika diperlukan

    • Contoh: Membebankan pembayaran terlebih dahulu, mengembalikan jika persediaan tidak tersedia

Persyaratan Idempotensi

Semua operasi dan kompensasi harus idempoten. Ini memastikan bahwa mengulang operasi yang gagal tidak menyebabkan efek ganda.

func (s *PaymentService) Refund(paymentID string) error {
    // Periksa apakah sudah dikembalikan
    payment, err := s.getPayment(paymentID)
    if err != nil {
        return err
    }
    
    if payment.Status == "refunded" {
        return nil // Sudah dikembalikan, idempoten
    }
    
    // Proses pengembalian
    return s.processRefund(paymentID)
}

Praktik Terbaik

1. Manajemen Status Saga

Pertahankan status setiap instance saga untuk melacak kemajuan dan memungkinkan pemulihan. Ketika menyimpan status saga ke database, memilih ORM yang tepat sangat penting untuk kinerja dan pemeliharaan. Untuk implementasi berbasis PostgreSQL, pertimbangkan perbandingan dalam Membandingkan Go ORMs untuk PostgreSQL: GORM vs Ent vs Bun vs sqlc untuk memilih yang paling sesuai dengan kebutuhan penyimpanan status saga Anda:

type SagaState struct {
    ID           string
    Status       SagaStatus
    Steps        []SagaStep
    CurrentStep  int
    CreatedAt    time.Time
    UpdatedAt    time.Time
}

type SagaStep struct {
    Service     string
    Operation   string
    Status      StepStatus
    Compensated bool
    Data        map[string]interface{}
}

2. Penanganan Timeout

Implementasikan timeout untuk setiap langkah untuk mencegah saga terjebak tanpa batas waktu:

type SagaOrchestrator struct {
    timeout time.Duration
}

func (o *SagaOrchestrator) ExecuteWithTimeout(step SagaStep) error {
    ctx, cancel := context.WithTimeout(context.Background(), o.timeout)
    defer cancel()
    
    done := make(chan error, 1)
    go func() {
        done <- step.Execute()
    }()
    
    select {
    case err := <-done:
        return err
    case <-ctx.Done():
        // Timeout terjadi, kompensasi
        if err := step.Compensate(); err != nil {
            return fmt.Errorf("kompensasi gagal: %w", err)
        }
        return fmt.Errorf("langkah %s timeout setelah %v", step.Name(), o.timeout)
    }
}

3. Logika Ulang Coba

Implementasikan backoff eksponensial untuk kegagalan sementara:

func retryWithBackoff(operation func() error, maxRetries int) error {
    backoff := time.Second
    for i := 0; i < maxRetries; i++ {
        err := operation()
        if err == nil {
            return nil
        }
        
        if !isTransientError(err) {
            return err
        }
        
        time.Sleep(backoff)
        backoff *= 2
    }
    return fmt.Errorf("operasi gagal setelah %d ulang coba", maxRetries)
}

4. Sumber Acara untuk Status Saga

Gunakan sumber acara untuk mempertahankan jejak audit lengkap. Ketika mengimplementasikan penyimpanan acara dan mekanisme ulang, Go generics dapat membantu membuat kode penanganan acara yang aman tipe dan dapat digunakan kembali. Untuk pola lanjutan menggunakan generics dalam Go, lihat Generics dalam Go: Kasus Penggunaan dan Pola:

type SagaEvent struct {
    SagaID    string
    EventType string
    Payload   []byte
    Timestamp time.Time
    Version   int64
}

type SagaEventStore struct {
    store EventRepository
}

func (s *SagaEventStore) AppendEvent(sagaID string, eventType string, payload interface{}) error {
    data, err := json.Marshal(payload)
    if err != nil {
        return fmt.Errorf("gagal mengubah payload menjadi JSON: %w", err)
    }
    
    version, err := s.store.GetNextVersion(sagaID)
    if err != nil {
        return fmt.Errorf("gagal mendapatkan versi: %w", err)
    }
    
    event := SagaEvent{
        SagaID:    sagaID,
        EventType: eventType,
        Payload:   data,
        Timestamp: time.Now(),
        Version:   version,
    }
    
    return s.store.Save(event)
}

func (s *SagaEventStore) ReplaySaga(sagaID string) (*Saga, error) {
    events, err := s.store.GetEvents(sagaID)
    if err != nil {
        return nil, fmt.Errorf("gagal mendapatkan acara: %w", err)
    }
    
    saga := NewSaga()
    for _, event := range events {
        if err := saga.Apply(event); err != nil {
            return nil, fmt.Errorf("gagal menerapkan acara: %w", err)
        }
    }
    
    return saga, nil
}

5. Pemantauan dan Observabilitas

Implementasikan logging dan pelacakan menyeluruh:

func (o *OrderSagaOrchestrator) CreateOrder(order Order) error {
    span := tracer.StartSpan("saga.create_order")
    defer span.Finish()
    
    span.SetTag("saga.id", sagaID)
    span.SetTag("order.id", order.ID)
    
    logger.WithFields(log.Fields{
        "saga_id": sagaID,
        "order_id": order.ID,
        "step": "create_order",
    }).Info("Saga dimulai")
    
    // ... eksekusi saga
    
    return nil
}

Pola Umum dan Anti-Pola

Pola yang Harus Dikuti

  • Pola Koordinator Saga: Gunakan layanan khusus untuk orkestrasi
  • Pola Outbox: Pastikan publikasi acara yang andal
  • Kunci Idempotensi: Gunakan kunci unik untuk semua operasi
  • Mesin Status Saga: Model saga sebagai mesin status

Anti-Pola yang Harus Dihindari

  • Kompensasi Sinkron: Jangan menunggu kompensasi selesai
  • Saga Bersarang: Hindari saga memanggil saga lain (gunakan sub-saga alih-alih)
  • Status Bersama: Jangan berbagi status antara langkah saga
  • Langkah Berlangsung Lama: Pecah langkah yang terlalu lama

Alat dan Kerangka Kerja

Beberapa kerangka kerja dapat membantu mengimplementasikan pola Saga:

  • Temporal: Platform orkestrasi workflow dengan dukungan bawaan untuk Saga
  • Zeebe: Engine workflow untuk orkestrasi mikroservis
  • Eventuate Tram: Kerangka kerja Saga untuk Spring Boot
  • AWS Step Functions: Orkestrasi workflow tanpa server
  • Apache Camel: Kerangka integrasi dengan dukungan Saga

Untuk layanan orkestrator yang membutuhkan antarmuka CLI untuk manajemen dan pemantauan, Membangun Aplikasi CLI dalam Go dengan Cobra & Viper menyediakan pola yang sangat baik untuk membuat alat CLI untuk berinteraksi dengan orkestrator saga.

Ketika mendeploy mikroservis berbasis saga di Kubernetes, mengimplementasikan mesh layanan dapat secara signifikan meningkatkan observabilitas, keamanan, dan manajemen lalu lintas. Mengimplementasikan Mesh Layanan dengan Istio dan Linkerd menuturkan bagaimana mesh layanan melengkapi pola transaksi terdistribusi dengan menyediakan kebutuhan lintas seperti pelacakan terdistribusi dan circuit breaking.

Kapan Menggunakan Pola Saga

Gunakan pola Saga ketika:

  • ✅ Operasi mencakup beberapa mikroservis
  • ✅ Proses bisnis berlangsung lama
  • ✅ Konsistensi akhir dapat diterima
  • ✅ Anda perlu menghindari kunci terdistribusi
  • ✅ Layanan memiliki basis data independen

Hindari ketika:

  • ❌ Konsistensi kuat diperlukan
  • ❌ Operasi sederhana dan cepat
  • ❌ Semua layanan berbagi basis data yang sama
  • ❌ Logika kompensasi terlalu kompleks

Kesimpulan

Pola Saga sangat penting untuk mengelola transaksi terdistribusi dalam arsitektur mikroservis. Meskipun memperkenalkan kompleksitas, pola ini menyediakan solusi praktis untuk mempertahankan konsistensi data di antara batas layanan. Pilih orkestrasi untuk kontrol dan visibilitas yang lebih baik, atau koreografi untuk skalabilitas dan keterpisahan. Pastikan selalu bahwa operasi idempoten, implementasikan logika kompensasi yang tepat, dan pertahankan observabilitas menyeluruh.

Kunci keberhasilan implementasi Saga adalah memahami kebutuhan konsistensi Anda, merancang logika kompensasi dengan hati-hati, dan memilih pendekatan yang tepat untuk kasus penggunaan Anda. Dengan implementasi yang tepat, Saga memungkinkan Anda membangun mikroservis yang tangguh dan skalabel yang mempertahankan integritas data di sistem terdistribusi.

Tautan yang Berguna