Patrón Saga en Transacciones Distribuidas - Con Ejemplos en Go
Transacciones en microservicios con el patrón Saga
El patrón Saga proporciona una solución elegante al dividir las transacciones distribuidas en una serie de transacciones locales con acciones compensatorias.
En lugar de depender de bloqueos distribuidos que pueden bloquear operaciones a través de servicios, Saga permite la consistencia eventual a través de una secuencia de pasos reversibles, lo que lo hace ideal para procesos empresariales de larga duración.
En arquitecturas de microservicios, mantener la consistencia de datos a través de servicios es uno de los problemas más desafiantes. Las transacciones tradicionales ACID no funcionan cuando las operaciones abarcan múltiples servicios con bases de datos independientes, dejando a los desarrolladores buscando enfoques alternativos para garantizar la integridad de los datos.
Esta guía demuestra la implementación del patrón Saga en Go con ejemplos prácticos que cubren tanto el enfoque de orquestación como el de coreografía. Si necesitas una referencia rápida para fundamentos de Go, la Hoja de trucos de Go proporciona una visión general útil.
Esta imagen agradable fue generada por modelo AI Flux 1 dev.
Entendiendo el patrón Saga
El patrón Saga fue originalmente descrito por Hector Garcia-Molina y Kenneth Salem en 1987. En el contexto de microservicios, es una secuencia de transacciones locales donde cada transacción actualiza datos dentro de un solo servicio. Si cualquier paso falla, se ejecutan transacciones compensatorias para revertir los efectos de los pasos anteriores.
A diferencia de las transacciones distribuidas tradicionales que utilizan el compromiso en dos fases (2PC), Saga no mantiene bloqueos a través de servicios, lo que lo hace adecuado para procesos empresariales de larga duración. El intercambio es la consistencia eventual en lugar de la consistencia fuerte.
Características clave
- Sin bloqueos distribuidos: Cada servicio gestiona su propia transacción local
- Acciones compensatorias: Cada operación tiene un mecanismo de rollback correspondiente
- Consistencia eventual: El sistema eventualmente alcanza un estado coherente
- De larga duración: Adecuado para procesos que toman segundos, minutos o incluso horas
Enfoques de implementación de Saga
Hay dos enfoques principales para implementar el patrón Saga: orquestación y coreografía.
Patrón de orquestación
En la orquestación, un coordinador central (orquestador) gestiona todo el flujo de transacción. El orquestador es responsable de:
- Invocar servicios en el orden correcto
- Manejar fallos y disparar compensaciones
- Mantener el estado del saga
- Coordinar reintentos y tiempos de espera
Ventajas:
- Control y visibilidad centralizados
- Más fácil de entender y depurar
- Mejor manejo de errores y recuperación
- Prueba más sencilla del flujo general
Desventajas:
- Punto único de fallo (aunque esto puede mitigarse)
- Servicio adicional que mantener
- Puede convertirse en un cuello de botella para flujos complejos
Ejemplo en Go:
type OrderSagaOrchestrator struct {
orderService OrderService
paymentService PaymentService
inventoryService InventoryService
shippingService ShippingService
}
func (o *OrderSagaOrchestrator) CreateOrder(order Order) error {
sagaID := generateSagaID()
// Paso 1: Crear orden
orderID, err := o.orderService.Create(order)
if err != nil {
return err
}
// Paso 2: Reservar inventario
if err := o.inventoryService.Reserve(order.Items); err != nil {
o.orderService.Cancel(orderID) // Compensar
return err
}
// Paso 3: Procesar pago
paymentID, err := o.paymentService.Charge(order.CustomerID, order.Total)
if err != nil {
o.inventoryService.Release(order.Items) // Compensar
o.orderService.Cancel(orderID) // Compensar
return err
}
// Paso 4: Crear envío
if err := o.shippingService.CreateShipment(orderID); err != nil {
o.paymentService.Refund(paymentID) // Compensar
o.inventoryService.Release(order.Items) // Compensar
o.orderService.Cancel(orderID) // Compensar
return err
}
return nil
}
Patrón de coreografía
En la coreografía, no hay un coordinador central. Cada servicio sabe qué hacer y se comunica a través de eventos. Los servicios escuchan eventos y reaccionan en consecuencia. Este enfoque basado en eventos es especialmente poderoso cuando se combina con plataformas de streaming de mensajes como AWS Kinesis, que proporcionan infraestructura escalable para la distribución de eventos entre microservicios. Para una guía completa sobre la implementación de microservicios basados en eventos con Kinesis, vea Construyendo microservicios basados en eventos con AWS Kinesis.
Ventajas:
- Descentralizado y escalable
- Sin punto único de fallo
- Los servicios permanecen desacoplados
- Ajuste natural para arquitecturas basadas en eventos
Desventajas:
- Más difícil entender el flujo general
- Difícil de depurar y rastrear
- Manejo de errores complejo
- Riesgo de dependencias cíclicas
Ejemplo con arquitectura basada en eventos:
// Servicio de orden
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) // Compensación
}
// Servicio de pago
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 {
// Compensación: reembolso de pago
return s.client.Refund(event.PaymentID)
}
Estrategias de compensación
La compensación es el corazón del patrón Saga. Cada operación debe tener una compensación correspondiente que pueda revertir sus efectos.
Tipos de compensación
-
Operaciones reversibles: Operaciones que pueden ser directamente anuladas
- Ejemplo: Liberar inventario reservado, reembolsar pagos
-
Acciones compensatorias: Operaciones diferentes que logran el efecto inverso
- Ejemplo: Cancelar un pedido en lugar de eliminarlo
-
Compensación pesimista: Preasignar recursos que pueden ser liberados
- Ejemplo: Reservar inventario antes de cargar el pago
-
Compensación optimista: Ejecutar operaciones y compensar si es necesario
- Ejemplo: Cargar el pago primero, reembolsar si el inventario no está disponible
Requisitos de idempotencia
Todas las operaciones y compensaciones deben ser idempotentes. Esto garantiza que repetir una operación fallida no cause efectos duplicados.
func (s *PaymentService) Refund(paymentID string) error {
// Verificar si ya se ha reembolsado
payment, err := s.getPayment(paymentID)
if err != nil {
return err
}
if payment.Status == "refunded" {
return nil // Ya se reembolsó, idempotente
}
// Procesar reembolso
return s.processRefund(paymentID)
}
Buenas prácticas
1. Gestión del estado de Saga
Mantenga el estado de cada instancia de Saga para rastrear el progreso y permitir la recuperación. Al persistir el estado de Saga en una base de datos, elegir el ORM adecuado es crucial para el rendimiento y la mantenibilidad. Para implementaciones basadas en PostgreSQL, considere la comparación en Comparando ORMs de Go para PostgreSQL: GORM vs Ent vs Bun vs sqlc para seleccionar la mejor opción para sus necesidades de almacenamiento de estado de Saga:
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. Manejo de tiempos de espera
Implemente tiempos de espera para cada paso para evitar que los Sagas se queden colgados indefinidamente:
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():
// Se alcanzó el tiempo de espera, compensar
if err := step.Compensate(); err != nil {
return fmt.Errorf("compensación fallida: %w", err)
}
return fmt.Errorf("paso %s se agotó después de %v", step.Name(), o.timeout)
}
}
3. Lógica de reintento
Implemente un retroceso exponencial para fallos transitorios:
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("operación fallida después de %d reintentos", maxRetries)
}
4. Uso de event sourcing para el estado de Saga
Use event sourcing para mantener un registro completo de auditoría. Cuando se implementan almacenes de eventos y mecanismos de reproducción, los generales de Go pueden ayudar a crear código de manejo de eventos tipo seguro y reutilizable. Para patrones avanzados usando generales en Go, vea Generales en Go: Casos de uso y patrones.
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("falló al serializar carga útil: %w", err)
}
version, err := s.store.GetNextVersion(sagaID)
if err != nil {
return fmt.Errorf("falló al obtener versión: %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("falló al obtener eventos: %w", err)
}
saga := NewSaga()
for _, event := range events {
if err := saga.Apply(event); err != nil {
return nil, fmt.Errorf("falló al aplicar evento: %w", err)
}
}
return saga, nil
}
5. Monitoreo y observabilidad
Implemente un registro y seguimiento completos:
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 iniciada")
// ... ejecución de saga
return nil
}
Patrones comunes y anti-patrones
Patrones a seguir
- Patrón de coordinador de Saga: Use un servicio dedicado para la orquestación
- Patrón de salida: Asegure publicación confiable de eventos
- Claves de idempotencia: Use claves únicas para todas las operaciones
- Máquina de estado de Saga: Modele la Saga como una máquina de estado
Anti-patrones a evitar
- Compensación sincrónica: No espere a que la compensación se complete
- Sagas anidadas: Evite que las Sagas llamen a otras Sagas (use sub-Sagas en su lugar)
- Estado compartido: No comparta estado entre pasos de Saga
- Pasos de larga duración: Divida pasos que tomen demasiado tiempo
Herramientas y marcos
Varias herramientas pueden ayudar a implementar patrones de Saga:
- Temporal: Plataforma de orquestación de flujos con soporte integrado para Saga
- Zeebe: Motor de flujos para orquestación de microservicios
- Eventuate Tram: Marco de Saga para Spring Boot
- AWS Step Functions: Orquestación de flujos sin servidor
- Apache Camel: Marco de integración con soporte para Saga
Para servicios de orquestador que necesiten interfaces de CLI para gestión y monitoreo, Construyendo aplicaciones de CLI en Go con Cobra & Viper proporciona patrones excelentes para crear herramientas de línea de comandos para interactuar con orquestadores de Saga.
Cuando se despliegan microservicios basados en Saga en Kubernetes, implementar una red de servicios puede mejorar significativamente la observabilidad, la seguridad y la gestión del tráfico. Implementando red de servicios con Istio y Linkerd cubre cómo las redes de servicios complementan los patrones de transacciones distribuidas al proporcionar preocupaciones transversales como el seguimiento distribuido y el circuito de ruptura.
Cuándo usar el patrón Saga
Use el patrón Saga cuando:
- ✅ Las operaciones abarcan múltiples microservicios
- ✅ Procesos empresariales de larga duración
- ✅ La consistencia eventual es aceptable
- ✅ Necesita evitar bloqueos distribuidos
- ✅ Los servicios tienen bases de datos independientes
Evite cuando:
- ❌ Se requiere consistencia fuerte
- ❌ Las operaciones son simples y rápidas
- ❌ Todos los servicios comparten la misma base de datos
- ❌ La lógica de compensación es demasiado compleja
Conclusión
El patrón Saga es esencial para manejar transacciones distribuidas en arquitecturas de microservicios. Aunque introduce complejidad, proporciona una solución práctica para mantener la consistencia de datos a través de los límites de servicio. Elija la orquestación para un mejor control y visibilidad, o la coreografía para escalabilidad y desacoplamiento. Siempre asegúrese de que las operaciones sean idempotentes, implemente una lógica de compensación adecuada y mantenga una observabilidad completa.
La clave para una implementación exitosa de Saga es comprender sus requisitos de consistencia, diseñar cuidadosamente la lógica de compensación y elegir el enfoque adecuado para su caso de uso. Con una implementación adecuada, Saga le permite construir microservicios resistentes y escalables que mantengan la integridad de los datos en sistemas distribuidos.
Enlaces útiles
- Microservicios Patterns por Chris Richardson
- Patrón Saga - Martin Fowler
- Eventuate Tram Saga Framework
- Motor de flujos Temporal
- Documentación de AWS Step Functions
- Hoja de trucos de Go
- Generales en Go: Casos de uso y patrones
- Comparando ORMs de Go para PostgreSQL: GORM vs Ent vs Bun vs sqlc
- Construyendo aplicaciones de CLI en Go con Cobra & Viper
- Implementando red de servicios con Istio y Linkerd
- Construyendo microservicios basados en eventos con AWS Kinesis