Introdução aos Modos de Transação do Seata
Em arquiteturas de microsserviços, a garantia da consistência dos dados em operações que abrangem múltiplos serviços e bases de dados é um desafio central. O Seata (Simple Extensible Autonomous Transaction Architecture) é uma solução de transação distribuída que oferece diferentes modos para lidar com esse problema, cada um com suas características e cenários de aplicação específicos. Abaixo, exploramos os modos AT, TCC e Saga, detalhando seus princípios e implementações.
1. Modo AT (Automatic Transaction)
O modo AT do Seata é uma solução de transação distribuída baseada em compensação automática, inspirada no conceito de duas fases. Ele se destaca por sua baixa intrusão no código de negócio, operando de forma quase transparente ao desenvolvedor.
Princípios de Funcionamento do AT
- Fase de Bloqueio Local e Registro de Undo Log: Quando uma operação de escrita (UPDATE, DELETE, INSERT) é executada, o Seata, através de um proxy de datasource, intercepta o SQL. Antes da modificação, ele registra uma "imagem" dos dados (
before_image) e, após a modificação, registra a "imagem" dos dados modificados (after_image). Esses registros são armazenados em uma tabela deundo_logna mesma base de dados local. Além disso, um bloqueio local é estabelecido nos registros afetados. - Consistência com Bloqueio Global: Durante a transação global, o Seata aplica um bloqueio global sobre os registros que estão sendo modificados por uma transação de ramo. Este bloqueio global impede que outras transações globais modifiquem os mesmos registros, garantindo o isolamento necessário (leitura repetível para a transação global e evitando escritas perdidas).
- Commit da Transação Global: Se todas as transações de ramo (locais) forem bem-sucedidas, o Coordenador de Transações (TC) do Seata instrui os Gerenciadores de Recursos (RM) a fazerem commit de suas transações locais. Após o commit, os
undo_logscorrespondentes são apagados. - Rollback da Transação Global: Se alguma transação de ramo falhar ou o TC decidir pelo rollback, os RMs usam os
undo_logspreviamente registrados para gerar e executar SQLs de compensação reversa, restaurando os dados ao seu estado original (before_image).
Implementação no Seata AT
Componentes Internos e Interceptação
O Seata AT opera injetando interceptores no nível do framework ORM (como Mybatis ou Hibernate) ou JDBC.
// Exemplo simplificado de Interceptor de Operações SQL (Mybatis) para o Modo AT
public class InterceptorRegistroTransacional implements Interceptor {
@Override
public Object intercept(Invocation invocacao) throws Throwable {
MappedStatement instrucaoSql = (MappedStatement) invocacao.getArgs()[0];
Object parametro = invocacao.getArgs()[1];
// Apenas operações de UPDATE são interceptadas para demonstração
if (instrucaoSql.getSqlCommandType() != SqlCommandType.UPDATE) {
return invocacao.proceed();
}
BoundSql sqlVinculado = instrucaoSql.getBoundSql(parametro);
String sqlOriginal = sqlVinculado.getSql();
Map<String, Object> mapaParametros = (Map<String, Object>) parametro;
// 1. Capturar o estado dos dados ANTES da execução da atualização
String selectAntesSql = transformarUpdateParaSelect(sqlOriginal); // Transforma UPDATE em SELECT
List<Map<String, Object>> dadosAntes = executarConsultaNoDB(selectAntesSql, mapaParametros);
// 2. Executar a operação de atualização original
Object resultado = invocacao.proceed();
// 3. Capturar o estado dos dados DEPOIS da execução da atualização
List<Map<String, Object>> dadosDepois = executarConsultaNoDB(selectAntesSql, mapaParametros);
// 4. Salvar os registros de undo log para possível compensação
gerenciarLogDesfazer(instrucaoSql.getId(), dadosAntes, dadosDepois);
return resultado;
}
private String transformarUpdateParaSelect(String updateSql) {
// Lógica simplificada: extrai a tabela e a cláusula WHERE
// Ex: UPDATE tabela SET col=val WHERE id=1 -> SELECT * FROM tabela WHERE id=1
return updateSql.replaceFirst("UPDATE (\\w+) SET .*? WHERE (.*)", "SELECT * FROM $1 WHERE $2");
}
// Métodos auxiliares 'executarConsultaNoDB' e 'gerenciarLogDesfazer'
// seriam implementados para interagir com o banco de dados e o serviço de undo log do Seata.
// ...
}
// Gerenciamento do Ciclo de Vida da Transação Global (simplificado)
public class GerenciadorTransacaoSeata {
private static final ThreadLocal<String> idTransacaoAtual = new ThreadLocal<>();
public static void iniciar() {
String xid = UUID.randomUUID().toString();
idTransacaoAtual.set(xid);
// Registra a transação global com o Seata Transaction Coordinator (TC)
ServicoTC.registrarTransacaoGlobal(xid);
}
public static void efetivar() {
String xid = idTransacaoAtual.get();
// Sinaliza o TC para confirmar todas as transações de ramo
ServicoTC.confirmarTransacaoGlobal(xid);
// Limpa os logs de desfazimento após o commit bem-sucedido
LogDesfazerDAO.limparLogs(xid);
idTransacaoAtual.remove();
}
public static void reverter() {
String xid = idTransacaoAtual.get();
// Sinaliza o TC para reverter todas as transações de ramo
ServicoTC.reverterTransacaoGlobal(xid);
// Aciona a compensação usando os logs de desfazimento
LogDesfazerDAO.executarCompensacao(xid);
idTransacaoAtual.remove();
}
}
Estrutura do Undo Log
Os logs de desfazimento são cruciais para a capacidade de compensação automática do modo AT.
CREATE TABLE seata_undo_log (
log_id BIGINT AUTO_INCREMENT PRIMARY KEY,
xid VARCHAR(128) NOT NULL COMMENT 'ID da transação global',
branch_id BIGINT NOT NULL COMMENT 'ID da transação de ramo',
log_status INT NOT NULL COMMENT 'Estado do log: 0-normal, 1-ready, 2-committed, 3-rollback',
rollback_info MEDIUMBLOB NOT NULL COMMENT 'Informações de rollback (before/after images)',
log_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
log_modified TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY ux_xid_branch (xid, branch_id)
);
// Repositório/Mapper para a tabela de undo log
@Mapper
public interface IUndoLogRepository {
@Insert("INSERT INTO seata_undo_log (xid, branch_id, log_status, rollback_info) " +
"VALUES (#{xid}, #{branchId}, #{logStatus}, #{rollbackInfo})")
void salvarLogDesfazer(UndoLogEntry logEntry);
@Delete("DELETE FROM seata_undo_log WHERE xid = #{xid} AND branch_id = #{branchId}")
void excluirLogPorRamo(String xid, Long branchId);
}
Uso em Serviços de Negócio
// Exemplo de Serviço de Negócio utilizando o modo AT
@Service
public class ServicoDeVendas implements IServicoDeVendas {
@Autowired
private EstoqueRepository estoqueRepo;
@Autowired
private PedidoRepository pedidoRepo;
@GlobalTransactional // Anotação que demarca o início da transação global Seata
public void realizarCompra(String codItem, int quantidade) {
// Reduzir o estoque (esta operação será automaticamente interceptada pelo Seata AT)
estoqueRepo.atualizarEstoque(codItem, -quantidade);
// Criar um novo pedido (também interceptado)
pedidoRepo.criarNovoPedido(codItem, quantidade);
// Simular uma falha para testar o rollback
// if (true) throw new RuntimeException("Falha simulada no pagamento!");
}
}
Mecanismo de Bloqueio Global
Para garantir o isolamento adequado, o Seata AT implementa um bloqueio global que previne conflitos entre transações distribuídas.
// Interceptor de Bloqueio Global (simplificado)
public class InterceptorDeBloqueio implements Interceptor {
@Override
public Object intercept(Invocation invocacao) throws Throwable {
MappedStatement instrucao = (MappedStatement) invocacao.getArgs()[0];
// Verifica se a operação requer bloqueio global (ex: UPDATEs em uma transação global ativa)
if (GlobalTxContext.isActive() && operacaoModificaDados(instrucao)) {
BoundSql sqlOriginal = instrucao.getBoundSql(invocacao.getArgs()[1]);
String sqlComBloqueio = sqlOriginal.getSql() + " FOR UPDATE"; // Adiciona FOR UPDATE
// Cria um novo MappedStatement com o SQL modificado
MappedStatement instrucaoModificada = MappedStatementUtil.clonarEModificarSql(instrucao, sqlComBloqueio);
invocacao.getArgs()[0] = instrucaoModificada; // Substitui a instrução original
}
return invocacao.proceed();
}
private boolean operacaoModificaDados(MappedStatement ms) {
return ms.getSqlCommandType() == SqlCommandType.UPDATE ||
ms.getSqlCommandType() == SqlCommandType.DELETE ||
ms.getSqlCommandType() == SqlCommandType.INSERT;
}
}
2. Modo TCC (Try-Confirm-Cancel)
O modo TCC exige que o desenvolvedor defina explicitamente três fases para cada operação de negócio: Try, Confirm e Cancel. É um padrão mais intrusivo, mas oferece maior controle e pode alcançar uma consistência mais forte em cenários específicos.
Definição das Fases TCC
- Try (Tentativa): Esta fase é responsável por tentar reservar ou congelar os recursos necessários, verificar as pré-condições para a execução da transação e registrar a operação. Ela deve ser idempotente e garantir que os recursos estarão disponíveis para a fase Confirm.
- Confirm (Confirmação): Esta fase é executada se todas as fases Try de todos os serviços participantes forem bem-sucedidas. Ela efetiva as operações de negócio, consumindo os recursos reservados na fase Try. Deve ser idempotente.
- Cancel (Cancelamento): Esta fase é executada se alguma fase Try falhar ou se a transação global precisar ser revertida por qualquer motivo. Ela libera os recursos reservados na fase Try. Deve ser idempotente.
Exemplo de Interface TCC
/**
* Interface de serviço para operações TCC (Try-Confirm-Cancel).
* As anotações Seata @TwoPhaseBusinessAction direcionam o fluxo.
*/
public interface IServicoNegocioTcc {
@TwoPhaseBusinessAction(name = "processarRecurso", commitMethod = "confirmarProcessamento", rollbackMethod = "cancelarProcessamento")
boolean tentarProcessar(BusinessActionContext contextoAcao, String idRecurso, BigDecimal valor); // Fase Try
boolean confirmarProcessamento(BusinessActionContext contextoAcao); // Fase Confirm
boolean cancelarProcessamento(BusinessActionContext contextoAcao); // Fase Cancel
}
Implementação de Serviço TCC para Transferência de Fundos
// Implementação concreta do serviço de transferência de fundos utilizando TCC
@Service
public class ServicoContasTcc implements IServicoContasTcc {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
@Transactional // Cada fase TCC deve ser uma transação local
@TwoPhaseBusinessAction(name = "debitarConta", commitMethod = "confirmarDebito", rollbackMethod = "cancelarDebito")
public boolean tentarDebitar(BusinessActionContext contexto, String contaOrigem, BigDecimal quantia) {
// Fase Try: Congela um valor na conta de origem (deduz do saldo_disponivel e adiciona ao saldo_congelado)
// Isso garante que o saldo está disponível e o recurso é "reservado".
int linhasAfetadas = jdbcTemplate.update(
"UPDATE conta SET saldo_disponivel = saldo_disponivel - ?, saldo_congelado = saldo_congelado + ? WHERE numero_conta = ? AND saldo_disponivel >= ?",
quantia, quantia, contaOrigem, quantia);
// Armazenar parâmetros no contexto para uso nas fases Confirm/Cancel
contexto.setActionContext("contaOrigem", contaOrigem);
contexto.setActionContext("quantia", quantia);
return linhasAfetadas > 0;
}
@Override
@Transactional
public boolean confirmarDebito(BusinessActionContext contexto) {
// Fase Confirm: Move o valor do saldo_congelado para efetivar o débito
String contaOrigem = (String) contexto.getActionContext("contaOrigem");
BigDecimal quantia = (BigDecimal) contexto.getActionContext("quantia");
return jdbcTemplate.update(
"UPDATE conta SET saldo_congelado = saldo_congelado - ? WHERE numero_conta = ?",
quantia, contaOrigem) > 0;
}
@Override
@Transactional
public boolean cancelarDebito(BusinessActionContext contexto) {
// Fase Cancel: Desfaz o congelamento e restaura o saldo_disponivel
String contaOrigem = (String) contexto.getActionContext("contaOrigem");
BigDecimal quantia = (BigDecimal) contexto.getActionContext("quantia");
return jdbcTemplate.update(
"UPDATE conta SET saldo_congelado = saldo_congelado - ?, saldo_disponivel = saldo_disponivel + ? WHERE numero_conta = ?",
quantia, quantia, contaOrigem) > 0;
}
}
3. Modo Saga
O padrão Saga é uma solução para transações de longa duração em microsserviços, onde cada passo é uma transação local. A consistência final é alcançada por meio de uma sequência de operações e suas respectivas operações de compensação, sem a necessidade de bloqueios de longa duração.
Mecanismo do Saga
- Sequência de Transações Locais: Uma Saga é composta por uma sequência de transações locais, onde cada transação local atualiza o banco de dados e publica um evento ou invoca o próximo serviço.
- Ações de Compensação: Para cada transação local (ação principal), existe uma ação de compensação correspondente. Se alguma transação local no fluxo Saga falhar, as ações de compensação das transações locais já concluídas são executadas na ordem inversa para reverter o estado do sistema.
- Orquestração vs. Coreografia: O Saga pode ser implementado via:
- Orquestração: Um serviço orquestrador central gerencia e coordena o fluxo da Saga, invocando os serviços participantes e suas ações de compensação.
- Coreografia: Os serviços participantes reagem a eventos publicados por outros serviços, formando uma cadeia de eventos e compensações sem um orquestrador central explícito.
Exemplo de Fluxo Saga com Seata (Orquestração)
// Serviço de Reserva de Hotel com compensação
@Service
public class ServicoReservaHotel {
@SagaService(compensate = "cancelarReservaHotel") // Indica o método de compensação
public void reservarHotel(String idViagem, String idUsuario) {
System.out.println("Hotel reservado para viagem " + idViagem + " por " + idUsuario);
// Lógica de negócio para reservar hotel
// Simular falha para testar compensação
// if (Math.random() < 0.5) throw new RuntimeException("Falha na reserva de hotel!");
}
public void cancelarReservaHotel(String idViagem, String idUsuario) {
System.out.println("Reserva de hotel cancelada para viagem " + idViagem + " por " + idUsuario);
// Lógica de negócio para cancelar reserva de hotel
}
}
// Serviço de Reserva de Voo com compensação
@Service
public class ServicoReservaVoo {
@SagaService(compensate = "cancelarReservaVoo")
public void reservarVoo(String idViagem, String idUsuario) {
System.out.println("Voo reservado para viagem " + idViagem + " por " + idUsuario);
// Lógica de negócio para reservar voo
}
public void cancelarReservaVoo(String idViagem, String idUsuario) {
System.out.println("Reserva de voo cancelada para viagem " + idViagem + " por " + idUsuario);
// Lógica de negócio para cancelar reserva de voo
}
}
// Orquestrador da Saga de Viagem
@Service
public class OrquestradorViagemSaga {
@Autowired
private ServicoReservaHotel servicoHotel;
@Autowired
private ServicoReservaVoo servicoVoo;
@SagaStart // Marca este método como o início de uma Saga
public void organizarViagemCompleta(String idViagem, String idUsuario) {
System.out.println("Iniciando organização da viagem completa: " + idViagem);
servicoHotel.reservarHotel(idViagem, idUsuario);
servicoVoo.reservarVoo(idViagem, idUsuario);
System.out.println("Organização da viagem completa finalizada (tentativa) para: " + idViagem);
}
}
Comparativo entre os Modos AT, TCC e Saga
| Característica | Modo AT | Modo TCC | Modo Saga |
|---|---|---|---|
| Consistência | Consistência final (com isolamento por bloqueio global) | Forte consistência (por reserva explícita de recursos) | Consistência final |
| Intrusão no Código | Baixa intrusão, quase transparente | Alta intrusão (exige implementação das 3 fases) | Média intrusão (exige definição de métodos de compensação) |
| Cenários de Uso | Operações CRUD em bases de dados tradicionais (ex: sistemas de estoque, pedidos) | Transações financeiras críticas (ex: débitos/créditos em contas) | Processos de negócio longos e complxeos (ex: fluxos de aprovação, logística) |
| Mecanismo de Bloqueio | Bloqueio global (prejudica a concorrência em alta carga) | Sem bloqueio de longa duração (opera com reserva de recursos) | Sem bloqueio de longa duração (opera com compensação) |
| Estratégia de Rollback | Automática (via undo logs gerados) | Manual (implementação da fase Cancel) | Event-driven ou orquestrada (via métodos de compensação) |
Configuração Essencial do Seata (application.yml)
Para integrar o Seata em uma aplicação Spring Boot, são necessárias configurações básicas, como a habilitação, ID da aplicação, grupo de serviços de transação e informações do registro/configuração (ex: Nacos).
seata:
enabled: true
application-id: servico-compras
tx-service-group: meu_grupo_seata # Nome do grupo de serviços de transação
service:
vgroup-mapping:
meu_grupo_seata: default_seata_cluster # Mapeamento do grupo para um cluster TC
enable-degrade: false # Habilita/desabilita modo degradado
disable-global-transaction: false # Habilita/desabilita transações globais
config:
type: nacos # Tipo de registro de configuração (Nacos, File, Redis, etc.)
nacos:
server-addr: 127.0.0.1:8848 # Endereço do servidor Nacos
registry:
type: nacos # Tipo de registro de serviço (Nacos, File, Redis, etc.)
nacos:
server-addr: 127.0.0.1:8848 # Endereço do servidor Nacos
Recomendações para Seleção de Modo em Produção
A escolha do modo de transação do Seata depende dos requisitos de negócio e técnicos:
- Modo AT: Ideal para a maioria das operações de banco de dados SQL que não exigem consistência financeira estrita, como gerenciamento de estoque, pedidos simples ou dados cadastrais. Oferece alta produtividade devido à baixa intrusão, mas o bloqueio global pode ser um gargalo em cenários de alta concorrência.
- Modo TCC: Apropriado para sistemas financeiramente críticos onde a forte consistência é imperativa e há um controle explícito sobre a reserva de recursos, como transações de débito/crédito em contas bancárias. Exige maior esforço de desenvolvimento para implementar as três fases.
- Modo Saga: Perfeito para processos de negócio de longa duração que envolvem múltiplos microsserviços e bases de dados, onde a consistência final é aceitável. Permite a continuação do processamento mesmo após falhas, utilizando compensações. É adequado para fluxos complexos como ordens de viagem, processamento de pagamentos em várias etapas ou cadeias de suprimentos.
Muitas vezes, uma abordagem híbrida é a mais eficaz, utilizando TCC para o "coração" financeiro da aplicação, AT para operações secundárias e Saga para orquestrar fluxos de trabalho complexos de longa duração.