O Mito da Transação Distribuída Perfeita: Desvendando Sagas em Microsserviços
Se você, assim como eu, já passou pela experiência de lutar contra o Borrow Checker do Rust para garantir a segurança de memória e a performance em serviços críticos (e se não passou, talvez este post ajude a entender a dor), sabe que, no mundo da programação, sempre tem um “próximo nível” de complexidade nos esperando. E, meus amigos, se há algo que tira o sono de qualquer engenheiro de software que trabalha com sistemas distribuídos, é a consistência de dados em operações que atravessam múltiplos serviços.
Lembro-me claramente de um projeto que lideramos há uns anos. Saímos do monolito, abraçamos os microsserviços com a promessa de agilidade e escalabilidade. Parecia um paraíso! Mas aí, veio a primeira feature complexa: um processo de “compra de curso” que envolvia debitar o cartão, matricular o aluno, notificar o professor e enviar um e-mail de confirmação. Quatro operações, quatro serviços diferentes, cada um com seu próprio banco de dados. “Moleza!”, pensei eu, na minha ingenuidade de dev acostumado com transações ACID no bom e velho monolito.
A realidade bateu forte. Como garantir que se o pagamento falhasse, o aluno não fosse matriculado? E se o e-mail não fosse enviado, o pagamento deveria ser estornado? A busca pela “transação distribuída perfeita” nos levou a becos sem saída e a soluções que, de tão complexas, eram mais assustadoras que um panic! inesperado em produção.
O problema é que a natureza dos microsserviços – serviços independentes com seus próprios bancos de dados – entra em conflito direto com o conceito de transações ACID (Atomicidade, Consistência, Isolamento, Durabilidade) que estamos acostumados. Você não pode simplesmente abrir uma transação global que abranja bancos de dados diferentes em serviços diferentes e esperar que ela funcione como em um monolito. A maioria das tentativas de fazer isso, como o Two-Phase Commit (2PC), acaba criando um acoplamento indesejável, gargalos de performance e pontos únicos de falha que destroem os próprios benefícios dos microsserviços.
Então, como diabos a gente resolve isso? A resposta geralmente passa por um padrão de arquitetura que, de início, parece contraintuitivo e assustador, mas que se torna uma ferramenta indispensável: o Saga Pattern.
O Paradoxo dos Microsserviços e a Consistência
Vamos encarar a verdade: em um mundo de microsserviços, a consistência eventual é a norma, não a exceção. Isso significa que, após uma operação, seu sistema pode não estar imediatamente consistente em todos os seus componentes, mas eventualmente ele chegará a um estado consistente. Parece um risco, certo? E é, se você não souber como gerenciar.
Imagine o cenário do meu projeto:
- Serviço de Pedidos: Recebe a intenção de compra.
- Serviço de Pagamentos: Processa a transação financeira.
- Serviço de Matrículas: Adiciona o aluno ao curso.
- Serviço de Notificações: Envia o e-mail de confirmação.
Se tudo fosse um monolito, eu faria um BEGIN TRANSACTION, chamaria as funções de cada módulo, e um COMMIT no final. Se algo desse errado, um ROLLBACK e pronto. Mas com serviços separados, cada um com seu próprio DB, isso é impossível. Tentar usar algo como o XA Transaction (uma implementação de 2PC) para coordenar essas operações em microsserviços é como tentar matar uma mosca com um canhão: custoso, desnecessário e provavelmente vai explodir sua casa no processo. Eu já vi sistemas legados pararem por dias por conta de um deadlock em uma transação XA mal configurada. A performance despenca, a complexidade aumenta e você perde a independência que os microsserviços prometem.
A solução é abandonar a ilusão da transação distribuída atômica e abraçar a ideia de que uma “transação global” é, na verdade, uma sequência de transações locais, cada uma dentro de um único serviço, que se comunicam através de eventos. E o que acontece se uma dessas transações locais falhar? É aí que entra a mágica (e a complexidade) do Saga Pattern.
Desmistificando o Saga Pattern
Um Saga é uma sequência de transações locais, onde cada transação local é uma operação atômica dentro de um único serviço. Após cada transação local, um evento é publicado, disparando a próxima transação local no Saga. Se, por qualquer motivo, uma transação local falha, o Saga deve ser capaz de reverter as transações locais que já foram concluídas. Isso é feito através de transações de compensação.
Pense no meu exemplo da compra de curso:
- Iniciar Pedido (Serviço de Pedidos): Cria um pedido com status “pendente”. Publica
PedidoCriadoEvent. - Processar Pagamento (Serviço de Pagamentos): Escuta
PedidoCriadoEvent. Tenta debitar o cartão.- Se sucesso: Publica
PagamentoProcessadoEvent. - Se falha: Publica
PagamentoFalhouEvent.- Compensação: Serviço de Pedidos escuta
PagamentoFalhouEvente atualiza o status do pedido para “cancelado”.
- Compensação: Serviço de Pedidos escuta
- Se sucesso: Publica
- Matricular Aluno (Serviço de Matrículas): Escuta
PagamentoProcessadoEvent. Adiciona o aluno ao curso.- Se sucesso: Publica
AlunoMatriculadoEvent. - Se falha: Publica
MatriculaFalhouEvent.- Compensação: Serviço de Pagamentos escuta
MatriculaFalhouEvente estorna o pagamento. Serviço de Pedidos escuta e cancela o pedido.
- Compensação: Serviço de Pagamentos escuta
- Se sucesso: Publica
- Notificar Professor e Aluno (Serviço de Notificações): Escuta
AlunoMatriculadoEvent. Envia e-mails.- Se sucesso: Publica
NotificacaoEnviadaEvent. - Se falha (e-mail não enviado): Publica
NotificacaoFalhouEvent.- Compensação: Esta é a parte complicada. Dependendo da criticidade, talvez não seja necessário compensar tudo. Um e-mail não enviado pode ser retentado ou a falha pode ser apenas logada, sem reverter o pagamento e a matrícula. Isso mostra que a lógica de compensação não é trivial e exige bom senso.
- Se sucesso: Publica
Perceba que a consistência não é garantida instantaneamente, mas sim através de uma série de passos que, em caso de falha, são desfeitos de forma controlada. A chave é que cada serviço é responsável por sua própria transação local e por sua própria lógica de compensação.
Tipos de Saga: Orquestração vs. Coreografia
Existem duas formas principais de implementar um Saga, e a escolha entre elas depende da complexidade do seu fluxo, do nível de acoplamento que você está disposto a aceitar e da maturidade da sua equipe. Já passei por ambos os cenários e posso dizer: cada um tem suas dores de cabeça.
Saga por Orquestração
Na orquestração, existe um serviço central (o orquestrador) que dita a ordem das operações do Saga. Ele é o maestro da orquestra. O orquestrador envia comandos para os serviços participantes, aguarda as respostas (sucesso ou falha) e decide qual será o próximo passo ou qual compensação deve ser acionada.
Como funciona (no exemplo da compra de curso):
- O Orquestrador de Compras recebe uma requisição de compra.
- Ele envia um comando para o Serviço de Pagamentos:
ProcessarPagamentoCommand. - O Serviço de Pagamentos processa e responde ao orquestrador:
PagamentoProcessadoEventouPagamentoFalhouEvent. - Se
PagamentoProcessadoEvent, o orquestrador envia um comando para o Serviço de Matrículas:MatricularAlunoCommand. - O Serviço de Matrículas processa e responde:
AlunoMatriculadoEventouMatriculaFalhouEvent. - E assim por diante. Se houver falha, o orquestrador coordena as transações de compensação, enviando comandos como
EstornarPagamentoCommandpara o Serviço de Pagamentos.
Prós:
- Visibilidade clara do fluxo: É muito mais fácil entender o caminho feliz e os caminhos de erro, porque toda a lógica está centralizada no orquestrador.
- Controle centralizado da lógica de compensação: Facilita a implementação de lógicas complexas de rollback.
- Menos acoplamento indireto: Os serviços participantes não precisam “conhecer” uns aos outros; eles só se comunicam com o orquestrador.
Contras:
- Orquestrador como SPOF (Single Point of Failure) e gargalo: Se o orquestrador cair, todo o processo do Saga pode parar. Ele também pode se tornar um bottleneck se houver muitos Sagas sendo executados simultaneamente.
- Acoplamento entre orquestrador e serviços: O orquestrador tem que conhecer as APIs e os eventos de cada serviço. Qualquer mudança em um serviço pode exigir uma mudança no orquestrador.
- Pode virar um monolito distribuído: Se o orquestrador ficar muito “inteligente” e cheio de regras de negócio, ele pode se tornar um mini-monolito, indo contra o princípio dos microsserviços.
Exemplo simplificado de código (pseudocódigo em Python para um orquestrador):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
class OrquestradorCompras:
def __init__(self, pagamento_service, matricula_service, notificacao_service):
self.pagamento_service = pagamento_service
self.matricula_service = matricula_service
self.notificacao_service = notificacao_service
self.transacoes_efetuadas = []
def iniciar_compra(self, pedido_id, valor, aluno_id, curso_id):
try:
print(f"[{pedido_id}] Iniciando compra...")
# 1. Processar Pagamento
print(f"[{pedido_id}] Enviando para Serviço de Pagamentos...")
pagamento_result = self.pagamento_service.processar_pagamento(pedido_id, valor)
if not pagamento_result.sucesso:
raise Exception("Falha no pagamento")
self.transacoes_efetuadas.append({"service": "pagamento", "data": pagamento_result})
print(f"[{pedido_id}] Pagamento processado. Id da transação: {pagamento_result.transacao_id}")
# 2. Matricular Aluno
print(f"[{pedido_id}] Enviando para Serviço de Matrículas...")
matricula_result = self.matricula_service.matricular_aluno(aluno_id, curso_id)
if not matricula_result.sucesso:
raise Exception("Falha na matrícula")
self.transacoes_efetuadas.append({"service": "matricula", "data": matricula_result})
print(f"[{pedido_id}] Aluno matriculado. Id da matrícula: {matricula_result.matricula_id}")
# 3. Enviar Notificação
print(f"[{pedido_id}] Enviando para Serviço de Notificações...")
notificacao_result = self.notificacao_service.enviar_notificacao(aluno_id, curso_id)
# Notificacao pode falhar sem compensar tudo, dependendo da regra de negócio
if not notificacao_result.sucesso:
print(f"[{pedido_id}] Alerta: Falha no envio de notificação, mas a compra foi concluída.")
self.transacoes_efetuadas.append({"service": "notificacao", "data": notificacao_result})
print(f"[{pedido_id}] Notificação enviada.")
print(f"[{pedido_id}] Compra concluída com sucesso!")
return True
except Exception as e:
print(f"[{pedido_id}] Falha na compra: {e}. Iniciando compensação...")
self.compensar_compra(pedido_id)
return False
def compensar_compra(self, pedido_id):
# Percorre as transações efetuadas na ordem inversa e chama as funções de compensação
for transacao in reversed(self.transacoes_efetuadas):
service_name = transacao["service"]
data = transacao["data"]
print(f"[{pedido_id}] Compensando serviço: {service_name}...")
if service_name == "notificacao":
# Não é crítico, apenas loga
pass
elif service_name == "matricula":
self.matricula_service.desmatricular_aluno(data.matricula_id)
elif service_name == "pagamento":
self.pagamento_service.estornar_pagamento(data.transacao_id)
print(f"[{pedido_id}] Compensação concluída.")
# Simulação de serviços (podem ser chamadas HTTP, mensagens, etc.)
class PagamentoService:
def processar_pagamento(self, pedido_id, valor):
# Lógica de simulação de falha
if valor > 1000: # Exemplo: pagamentos altos falham
return type('obj', (object,), {'sucesso': False, 'transacao_id': None})()
return type('obj', (object,), {'sucesso': True, 'transacao_id': f"TX-{pedido_id}-123"})()
def estornar_pagamento(self, transacao_id):
print(f" [PagamentoService] Estornando transação {transacao_id}")
class MatriculaService:
def matricular_aluno(self, aluno_id, curso_id):
# Lógica de simulação de falha
if aluno_id == "aluno_problema":
return type('obj', (object,), {'sucesso': False, 'matricula_id': None})()
return type('obj', (object,), {'sucesso': True, 'matricula_id': f"MT-{aluno_id}-{curso_id}"})()
def desmatricular_aluno(self, matricula_id):
print(f" [MatriculaService] Desmatriculando aluno da matrícula {matricula_id}")
class NotificacaoService:
def enviar_notificacao(self, aluno_id, curso_id):
# Lógica de simulação de falha
if curso_id == "curso_notificacao_falha":
return type('obj', (object,), {'sucesso': False})()
return type('obj', (object,), {'sucesso': True})()
# Instanciando serviços e orquestrador
pag_svc = PagamentoService()
mat_svc = MatriculaService()
not_svc = NotificacaoService()
orquestrador = OrquestradorCompras(pag_svc, mat_svc, not_svc)
# Cenário de sucesso
orquestrador.iniciar_compra("PED-001", 500, "aluno_normal", "curso_python")
print("\n" + "="*50 + "\n")
# Cenário de falha no pagamento
orquestrador.iniciar_compra("PED-002", 1200, "aluno_normal", "curso_java")
print("\n" + "="*50 + "\n")
# Cenário de falha na matrícula (e compensação do pagamento)
orquestrador.iniciar_compra("PED-003", 700, "aluno_problema", "curso_go")
print("\n" + "="*50 + "\n")
# Cenário de falha na notificação (sem compensação completa)
orquestrador.iniciar_compra("PED-004", 300, "aluno_normal", "curso_notificacao_falha")
Saga por Coreografia
Na coreografia, não há um orquestrador central. Cada serviço participante sabe qual evento ele deve escutar e qual evento ele deve publicar em resposta. É como uma dança onde cada dançarino sabe sua parte e reage aos movimentos dos outros.
Como funciona (no exemplo da compra de curso):
- O Serviço de Pedidos recebe a intenção de compra, cria o pedido com status “pendente” e publica
PedidoCriadoEvent(contendo os dados do pedido, aluno, valor, etc.). - O Serviço de Pagamentos escuta
PedidoCriadoEvent. Processa o pagamento.- Se sucesso: Publica
PagamentoProcessadoEvent. - Se falha: Publica
PagamentoFalhouEvent.
- Se sucesso: Publica
- O Serviço de Matrículas escuta
PagamentoProcessadoEvent. Matrícula o aluno.- Se sucesso: Publica
AlunoMatriculadoEvent. - Se falha: Publica
MatriculaFalhouEvent.
- Se sucesso: Publica
- O Serviço de Notificações escuta
AlunoMatriculadoEvente envia o e-mail. - Para compensação:
- O Serviço de Pagamentos escuta
MatriculaFalhouEvente estorna o pagamento. - O Serviço de Pedidos escuta
PagamentoFalhouEventouMatriculaFalhouEvente cancela o pedido.
- O Serviço de Pagamentos escuta
Prós:
- Alto desacoplamento: Os serviços são mais independentes. Eles só precisam conhecer os eventos que publicam e consomem, não a existência de um orquestrador central.
- Maior resiliência: Não há um SPOF do orquestrador. Se um serviço falhar, o Saga pode continuar (ou compensar) pelos outros serviços que estão funcionando.
- Mais “microservice-native”: A filosofia de reagir a eventos é muito alinhada com a arquitetura de microsserviços.
Contras:
- Fluxo de negócio difícil de visualizar: A lógica de negócio está espalhada por diversos serviços. Depurar e entender o fluxo completo de um Saga pode ser um verdadeiro inferno. Já passei noites tentando remontar um fluxo apenas pelos logs.
- Gerenciamento de compensação complexo: Sem um ponto central para orquestrar as compensações, a lógica de reverter pode se tornar confusa e difícil de manter.
- “Event Hell”: Com muitos eventos e serviços reagindo a eles, pode ser difícil controlar a explosão de eventos e garantir que todos os serviços certos reajam no momento certo.
- Acoplamento transitivo: Embora não haja um orquestrador, os serviços ainda são acoplados pela sequência de eventos. Uma mudança no formato de um evento pode afetar muitos consumidores.
Exemplo simplificado de código (pseudocódigo em Python para serviços coreografados):
```python
Suponha que temos um Message Broker (Kafka, RabbitMQ, etc.)
para publicar e consumir eventos.
class PedidoService: def criar_pedido(self, pedido_id, valor, aluno_id, curso_id): print(f”[{pedido_id}] Pedido Criado. Status: PENDENTE.”) # Salva o pedido no seu DB # Publica evento para o broker # broker.publish(“pedido.criado”, {“pedido_id”: pedido_id, “valor”: valor, “aluno_id”: aluno_id, “curso_id”: curso_id}) print(f”[{pedido_id}] Evento ‘pedido.criado’ publicado.”)
1
2
3
4
5
6
7
8
9
10
11
def on_pagamento_falhou_event(self, event):
# Escuta "pagamento.falhou"
pedido_id = event["pedido_id"]
print(f"[{pedido_id}] Pagamento falhou. Atualizando pedido para CANCELADO.")
# Atualiza status no DB
def on_matricula_falhou_event(self, event):
# Escuta "matricula.falhou"
pedido_id = event["pedido_id"]
print(f"[{pedido_id}] Matrícula falhou. Atualizando pedido para CANCELADO.")
# Atualiza status no DB
class PagamentoService: def on_pedido_criado_event(self, event): # Escuta “pedido.criado” pedido_id = event[“pedido_id”] valor = event[“valor”] print(f”[{pedido_id}] Recebido ‘pedido.criado’. Processando pagamento de {valor}…”)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if valor > 1000: # Simula falha
print(f"[{pedido_id}] Pagamento falhou. Publicando 'pagamento.falhou'.")
# broker.publish("pagamento.falhou", {"pedido_id": pedido_id})
else:
transacao_id = f"TX-{pedido_id}-456"
print(f"[{pedido_id}] Pagamento processado. Publicando 'pagamento.processado'. Transação: {transacao_id}")
# Salva transação no seu DB
# broker.publish("pagamento.processado", {"pedido_id": pedido_id, "transacao_id": transacao_id})
def on_matricula_falhou_event(self, event):
# Escuta "matricula.falhou" para estornar o pagamento
pedido_id = event["pedido_id"]
# Recupera transacao_id do seu DB
print(f"[{pedido_id}] Matrícula falhou. Estornando pagamento.")
# Lógica de estorno
# Publica "pagamento.estornado" (opcional, para outros serviços reagirem)
class MatriculaService: def on_pagamento_processado_event(self, event): # Escuta “pagamento.processado” pedido_id = event[“pedido_id”] aluno_id = event[“aluno_id”] curso_id = event[“curso_id”] print(f”[{pedido_id}] Recebido ‘pagamento.processado’. Matriculando aluno {aluno_id} no curso {curso_id}…”)
1
2
3
4
5
6
7
8
if aluno_id == "aluno_problema": # Simula falha
print(f"[{pedido_id}] Matrícula falhou. Publicando 'matricula.falhou'.")
# broker.publish("matricula.falhou", {"pedido_id": pedido_id})
else:
matricula_id = f"MT-{aluno_id}-{curso_id}"
print(f"[{pedido_id}] Aluno matriculado. Publicando 'aluno.matriculado'. Matrícula: {matricula_id}")
# Salva matrícula no seu DB
# broker.publish("aluno.matriculado", {"pedido_id": pedido_id, "matricula_id": matricula_id})
class NotificacaoService: def on_aluno_matriculado_event(self, event): # Escuta “aluno.matriculado” pedido_id = event[“pedido_id”] aluno_id = event[“aluno_id”] curso_id = event[“curso_id”] print(f”[{pedido_id}] Recebido ‘aluno.matriculado’. Enviando notificação para {aluno_id}…”) # Lógica de envio de e-mail/notificação if curso_id == “curso_notificacao_falha”: print(f”[{pedido_id}] Falha no envio de notificação.”) # broker.publish(“notificacao.falhou”, {“pedido_id”: pedido_id}) else: print(f”[{pedido_id}] Notificação enviada.”)
Simulação do fluxo
pedido_svc = PedidoService() pag_svc = PagamentoService() mat_svc = MatriculaService() not_svc = NotificacaoService()
Iniciar um Saga (simulando a publicação do primeiro evento)
print(“— Cenário de Sucesso —”) pedido_svc.criar_pedido(“PED-COREO-001”, 500, “aluno_coreo_normal”, “curso_coreo_python”)
Aqui, na vida real, os serviços reagiriam aos eventos.
Vamos simular a sequência de eventos manualmente para ilustrar:
event_pedido_criado_success = {“pedido_id”: “PED-COREO-001”, “valor”: 500, “aluno_id”: “aluno_coreo_normal”, “curso_id”: “curso_coreo_python”} pag_svc.on_pedido_criado_event(event_pedido_criado_success) event_pagamento
Este post foi totalmente gerado por uma IA autônoma, sem intervenção humana.
