Java EE / Jakarta EE

Jakarta Transactions (JTA 2): transações gerenciadas pelo contêiner, propagação e XA

Intermediário11 min de leituraAtualizado

Esta página foi traduzida do inglês com a ajuda de IA e conferida com o original. Ler a versão em inglês

Jakarta Transactions (JTA) é o serviço que permite ao contêiner iniciar, confirmar e desfazer um trabalho que envolve um banco de dados, um broker de mensagens e qualquer outro recurso inscrito. Depois de ler este guia, você vai saber o que uma transação garante e o que os níveis de isolamento deixam em aberto, como cada atributo de propagação se comporta, quais exceções causam rollback e por quê, quando usar transações gerenciadas pelo bean e quanto custa o commit em duas fases do XA — além dos erros de autoinvocação e de transações longas que minam todo o resto.

O que você vai aprender

  • Explicar as propriedades ACID, os níveis de isolamento e as anomalias que cada nível permite
  • Aplicar @Transactional e @TransactionAttribute com o tipo de propagação certo
  • Prever quando o contêiner faz rollback: exceções de sistema versus exceções de aplicação, rollbackOn e setRollbackOnly
  • Usar UserTransaction em transações gerenciadas pelo bean quando os limites declarativos não servem
  • Decidir quando o commit em duas fases do XA é necessário e quais alternativas existem

Uma transação é uma promessa que a plataforma faz em seu nome: tudo o que está dentro do limite acontece por inteiro ou não acontece, e ninguém mais vê o estado pela metade. No Jakarta EE, quem cumpre essa promessa é o serviço Jakarta Transactions — que todo mundo ainda chama de JTA —, aplicado pelo contêiner em volta de cada método de enterprise bean e de cada método CDI @Transactional, e que também coordena os recursos JPA, JMS e JDBC para que um único commit cubra todos eles. Os desenvolvedores raramente chamam o JTA diretamente: eles o configuram com anotações e só esbarram nas regras na primeira vez em que uma exceção não faz rollback, uma chamada aninhada não ganha sua própria transação ou uma mensagem é enviada para um pedido que nunca foi salvo. Este guia explica essas regras bem o suficiente para você conseguir prever esses casos.

O que uma transação garante

As propriedades ACID descrevem o contrato:

  • Atomicidade — todas as operações da transação têm efeito, ou nenhuma tem.
  • Consistência — a transação leva o banco de um estado válido a outro; as restrições são satisfeitas no commit.
  • Isolamento — transações concorrentes não enxergam o estado intermediário umas das outras (em um grau que depende do nível de isolamento).
  • Durabilidade — depois do commit, as alterações sobrevivem a uma queda.

O isolamento é a propriedade negociável, porque isolamento total custa throughput. O padrão SQL define quatro níveis de acordo com as anomalias que cada um permite:

NívelLeitura sujaLeitura não repetívelLeitura fantasmaCusto típico
READ UNCOMMITTEDpossívelpossívelpossívelo mais baixo; raramente usado
READ COMMITTEDevitadapossívelpossívelpadrão no PostgreSQL, Oracle, SQL Server
REPEATABLE READevitadaevitadapossível (evitada em alguns engines)padrão no MySQL InnoDB
SERIALIZABLEevitadaevitadaevitadao mais alto; novas tentativas em caso de conflito

Uma leitura suja (dirty read) enxerga dados não confirmados de outra transação; uma leitura não repetível obtém um valor diferente ao reler a mesma linha; uma leitura fantasma vê novas linhas surgirem ao repetir uma consulta por intervalo. READ COMMITTED combinado com lock otimista (@Version no JPA) é a combinação em que roda a maioria das aplicações de negócio: as leituras são baratas e as atualizações perdidas são detectadas no momento da escrita, em vez de evitadas com locks. Observe que o próprio JTA não define o nível — configure-o no data source.

Transações locais versus globais

Uma transação local envolve um único recurso: o commit() de uma única conexão JDBC. O banco faz todo o trabalho, e o JTA só diz quando. Uma transação global (distribuída) abrange vários recursos — dois bancos de dados, ou um banco e um broker de mensagens — e precisa de um coordenador capaz de fazer todos confirmarem ou todos desfazerem. O JTA fornece esse coordenador, o transaction manager, e os recursos participam pela interface XA (javax.transaction.xa.XAResource, que continuou no namespace do Java SE).

Em um servidor, os recursos são declarados como data sources e connection factories XA ou não XA. Inscrever um recurso não XA em uma transação junto com outro recurso é recusado ou tratado com uma "last resource commit optimisation" específica do fornecedor, que não é totalmente segura. A maioria das aplicações usa um único banco de dados e se sai melhor com um data source não XA: é mais simples, mais rápido, e o transaction manager continua oferecendo limites declarativos e rollback.

Transações gerenciadas pelo contêiner

O contêiner demarca a transação em volta de uma chamada de método e decide, com base em uma anotação, como o método se relaciona com qualquer transação que já esteja em andamento no chamador. Os seis tipos de propagação são os mesmos para @TransactionAttribute em enterprise beans e para @Transactional em beans CDI:

TipoO chamador tem uma transaçãoO chamador não tem
REQUIRED (padrão)Participar delaIniciar uma nova e fazer commit no retorno
REQUIRES_NEWSuspendê-la, rodar em uma nova e retomá-la depoisIniciar uma nova
MANDATORYParticipar delaLançar TransactionalException / EJBTransactionRequiredException
SUPPORTSParticipar delaRodar sem transação
NOT_SUPPORTEDSuspendê-la e rodar semRodar sem
NEVERLançar exceçãoRodar sem

REQUIRED é o certo para quase todo método de negócio: a chamada mais externa é dona da transação e as internas participam dela, então um serviço que chama dois repositories obtém uma única unidade atômica. REQUIRES_NEW serve para o trabalho que precisa ser confirmado mesmo que o chamador falhe — um registro de auditoria, um status de "falha no processamento" — e custa uma segunda conexão e, com XA, um segundo branch. MANDATORY documenta que um método nunca é um ponto de entrada. NOT_SUPPORTED e SUPPORTS são para caminhos somente leitura que não devem segurar uma transação, como consultas longas de relatório; NEVER protege contra chamadas feitas de dentro de uma.

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.transaction.Transactional.TxType;

@ApplicationScoped
public class PaymentService {

    @Inject OrderRepository orders;
    @Inject PaymentGateway gateway;
    @Inject AuditLog audit;

    @Transactional                                       // REQUIRED: one unit for the whole method
    public void pay(Long orderId, CardToken card) {
        Order order = orders.find(orderId);
        try {
            gateway.charge(order.total(), card);           // external call; cannot be rolled back
            order.markPaid();                              // dirty-checked, written at commit
        } catch (PaymentDeclinedException e) {
            audit.record(orderId, "declined: " + e.code());  // survives the rollback below
            throw e;                                       // marked rollbackOn, so the order stays unpaid
        }
    }
}

@ApplicationScoped
public class AuditLog {
    @PersistenceContext EntityManager em;

    @Transactional(TxType.REQUIRES_NEW)                  // its own transaction, committed independently
    public void record(Long orderId, String event) {
        em.persist(new AuditEntry(orderId, event));
    }
}

Para isso funcionar como esperado, PaymentDeclinedException precisa fazer rollback da transação externa. O que nos leva às regras.

O que causa rollback

As duas anotações distinguem dois tipos de falha. Uma exceção de sistema — qualquer RuntimeException ou Error — é tratada como bug ou falha de infraestrutura: o contêiner marca a transação para rollback e, nos enterprise beans, a encapsula em uma EJBException e descarta a instância. Uma exceção de aplicação — uma exceção checked — é tratada como um resultado de negócio: a transação faz commit e a exceção se propaga sem alteração.

Você pode mudar o padrão nos dois sentidos:

  • @Transactional(rollbackOn = PaymentDeclinedException.class) faz rollback nessa exceção checked; dontRollbackOn = ValidationException.class faz commit apesar dessa exceção runtime.
  • Nos enterprise beans, anote a classe da exceção com @ApplicationException(rollback = true); isso também impede que uma exceção runtime seja encapsulada em uma EJBException.
  • Programaticamente, injete TransactionManager ou EJBContext e chame setRollbackOnly(). A transação continua, mas não pode mais fazer commit — uma RollbackException é lançada no limite —, e é isso que uma etapa de validação lá no fundo da cadeia de chamadas deve fazer quando não pode lançar exceção.

Depois que uma transação é marcada para rollback, nada desfaz essa marcação. Uma surpresa comum: um método interno REQUIRED lança uma exceção runtime, o método externo a captura e segue em frente, e o commit externo falha com RollbackException — porque a falha interna já tinha condenado a transação compartilhada.

Timeouts

Todo servidor tem um timeout de transação padrão (geralmente 300 segundos); quando ele expira, uma thread "reaper" faz o rollback da transação e o próximo acesso a um recurso falha. Defina limites menores em métodos demorados com anotações específicas do fornecedor (@TransactionTimeout no WildFly) ou chamando UserTransaction.setTransactionTimeout no código gerenciado pelo bean. Uma transação que precisa de minutos geralmente indica um processamento em lote que deveria ser dividido.

Transações gerenciadas pelo bean

Quando os limites não coincidem com as chamadas de método — um loop de batch que faz commit a cada mil registros, um método que precisa decidir em tempo de execução se inicia uma transação —, assuma o controle com UserTransaction:

import jakarta.annotation.Resource;
import jakarta.ejb.Stateless;
import jakarta.ejb.TransactionManagement;
import jakarta.ejb.TransactionManagementType;
import jakarta.transaction.Status;
import jakarta.transaction.UserTransaction;

@Stateless
@TransactionManagement(TransactionManagementType.BEAN)   // the container will not demarcate for this bean
public class ImportJob {

    @Resource UserTransaction tx;
    @PersistenceContext EntityManager em;

    public void importAll(List<Row> rows) throws Exception {
        int i = 0;
        tx.begin();
        try {
            for (Row row : rows) {
                em.persist(toEntity(row));
                if (++i % 1000 == 0) {            // commit in chunks: bounded memory, bounded lock time
                    tx.commit();
                    em.clear();
                    tx.begin();
                }
            }
            tx.commit();
        } catch (Exception e) {
            if (tx.getStatus() == Status.STATUS_ACTIVE) tx.rollback();
            throw e;
        }
    }
}

UserTransaction fica disponível para qualquer componente via @Resource ou pelo nome JNDI java:comp/UserTransaction; beans CDI também podem injetá-la. Um bean stateless com transações gerenciadas pelo bean não pode retornar com uma transação ainda aberta — o contêiner faz rollback dela e registra um erro no log. O EntityManager com escopo de transação participa de qualquer transação que esteja ativa no momento em que é usado, então um em.clear() depois de cada commit impede que o contexto de persistência cresça.

TransactionSynchronizationRegistry é a terceira API que vale conhecer: ela permite registrar um callback para beforeCompletion e afterCompletion, que é a forma de enviar uma notificação só depois que o commit realmente deu certo. Os observers transacionais do CDI 4.0 fazem o mesmo de forma declarativa: void onOrderPaid(@Observes(during = TransactionPhase.AFTER_SUCCESS) OrderPaid event).

XA e commit em duas fases

Quando uma transação inscreveu dois recursos XA — por exemplo, um data source XA do PostgreSQL e uma connection factory do ActiveMQ Artemis —, o commit acontece em duas fases:

  1. Preparação. O transaction manager pede a cada recurso que se prepare: gravar em armazenamento durável tudo o que for necessário para o commit e votar sim ou não. Um recurso que vota sim precisa conseguir fazer o commit depois, mesmo após uma queda.
  2. Commit. Se todos votaram sim, o manager grava um registro de decisão no seu próprio log de transações e manda cada recurso fazer commit; caso contrário, manda todos desfazerem.

É o log que torna o protocolo recuperável: se o servidor cair entre as fases, ao reiniciar o manager lê o log e conclui os branches em dúvida (in-doubt). Esse também é o custo operacional do XA — o log precisa ficar em armazenamento confiável e sobreviver a redeploys e reinícios de contêiner, e um recurso que continua inacessível deixa transações in-doubt segurando locks até a recuperação terminar ou um administrador resolvê-las. O throughput é menor por causa das idas e voltas extras e das gravações forçadas em disco.

Antes de escolher XA, considere as alternativas:

  • Um único recurso. A maioria dos serviços precisa de um único banco de dados. Coloque as mensagens em uma tabela (transactional outbox) e deixe um relay publicá-las; a transação local cobre as duas coisas.
  • Consumidores idempotentes. Envie a mensagem depois do commit e faça os consumidores tolerarem uma duplicata ou uma mensagem que se refere a um registro que ainda não existe.
  • Compensação. Em processos de negócio longos que atravessam vários serviços, as sagas substituem a atomicidade por ações de compensação explícitas; o guia sobre transações distribuídas discute os trade-offs.

XA é a escolha certa quando os dois recursos são de fato críticos, o volume é moderado e existe maturidade operacional para monitorar a recuperação — o processamento clássico de message-driven bean mais banco de dados é exatamente esse caso, e o contêiner transforma isso em uma mudança de configuração de uma linha.

Como as peças se encaixam em tempo de execução

Considere um método de session bean com REQUIRED que persiste um pedido via JPA e envia uma mensagem JMS por um JMSContext injetado:

  1. O proxy pede ao transaction manager que inicie uma transação e a associa à thread atual.
  2. O primeiro uso do EntityManager obtém uma conexão do data source; a conexão é inscrita.
  3. O envio pelo JMSContext inscreve a sessão JMS; a mensagem fica em buffer, não é entregue.
  4. O método retorna normalmente. O contêiner chama a sincronização beforeCompletion do provider JPA, que faz o flush do contexto de persistência — o INSERT roda agora, e as violações de restrição aparecem aqui como PersistenceException.
  5. O manager faz o commit: em uma fase se só um recurso estiver inscrito, em duas fases caso contrário. A mensagem só fica visível para os consumidores depois do commit.
  6. O contexto de persistência é fechado; as entidades ficam desanexadas (detached).

O passo 4 explica por que as exceções do banco aparecem "depois" que seu código terminou, e por que um método @Transactional não consegue capturar as próprias falhas de flush — o flush acontece fora do corpo do método. Se você precisa reagir a uma violação de restrição, chame em.flush() explicitamente dentro do método e capture a exceção ali.

Erros comuns

  • Autoinvocação. Chamar this.record(...) em um método REQUIRES_NEW faz com que ele rode na transação do chamador, porque o proxy é ignorado. Injete o bean nele mesmo ou mova o método para outro bean.
  • Esperar que exceções checked façam rollback. Por padrão, elas fazem commit. Use rollbackOn ou @ApplicationException(rollback = true).
  • Capturar a exceção interna e fazer commit mesmo assim. A transação já está marcada; o commit lança RollbackException. Verifique getStatus() (ou getRollbackOnly() no contexto do enterprise bean) antes de continuar, ou use REQUIRES_NEW para o trabalho interno.
  • Transações longas. Manter uma transação aberta durante uma chamada HTTP remota ou enquanto o usuário pensa deixa locks e conexões ocupados. Mantenha o limite apenas em volta do acesso a dados.
  • Efeitos colaterais externos dentro da transação. Um e-mail enviado antes de um rollback não pode ser desenviado. Envie a partir de um observer AFTER_SUCCESS ou de um outbox.
  • XA por padrão. Configurar todos os data sources como XA custa desempenho e acrescenta uma recuperação para operar, em aplicações que nunca inscrevem um segundo recurso.
  • @Transactional em um método privado ou em um objeto que não é CDI. Sem proxy, sem interceptor, sem transação. Apenas métodos públicos em beans gerenciados.

Principais pontos

  • O JTA dá ao contêiner um transaction manager que demarca o trabalho e coordena os recursos inscritos; ACID é o contrato, e o nível de isolamento é definido no data source, não pelo JTA.
  • REQUIRED para métodos de negócio, REQUIRES_NEW para trabalho que precisa sobreviver à falha do chamador, MANDATORY e NEVER como proteções, SUPPORTS/NOT_SUPPORTED para caminhos somente leitura.
  • Exceções runtime fazem rollback, exceções checked fazem commit; mude isso com rollbackOn, @ApplicationException ou setRollbackOnly.
  • O flush acontece no commit, fora do seu método; chame em.flush() explicitamente se precisar tratar erros do banco no próprio lugar.
  • Use UserTransaction para batches divididos em blocos e limites decididos em tempo de execução; nunca deixe uma transação aberta quando um bean stateless retorna.
  • O commit em duas fases do XA é recuperável e correto, mas custa throughput e operação; prefira um único recurso mais um outbox, a menos que os dois recursos sejam críticos.
  • Chamadas via this ignoram o proxy e, com ele, todos os atributos transacionais.

Fontes e leituras complementares

Perguntas frequentes

Qual é a diferença entre @Transactional e @TransactionAttribute?
Os dois configuram o mesmo serviço do contêiner. @TransactionAttribute (jakarta.ejb) se aplica aos enterprise beans, cujos métodos são transacionais por padrão. @Transactional (jakarta.transaction, desde o JTA 1.2) é um interceptor binding do CDI que funciona em qualquer bean gerenciado, precisa ser adicionado explicitamente e traz os atributos rollbackOn e dontRollbackOn. Código novo em beans CDI usa @Transactional; os tipos de propagação são idênticos.
Por que minha transação não fez rollback quando lancei uma exceção?
Porque era uma exceção checked. Por padrão, as duas anotações fazem rollback em RuntimeException e Error e fazem commit em exceções checked. Declare @Transactional(rollbackOn = MyCheckedException.class), marque a classe da exceção com @ApplicationException(rollback = true) nos enterprise beans ou chame setRollbackOnly antes de retornar.
Preciso de XA se uso um único banco de dados e um único broker de mensagens?
Se a escrita no banco e a mensagem precisam ser atômicas, sim: são exatamente dois recursos, e só um commit em duas fases garante que os dois deem certo ou os dois falhem. As alternativas são o padrão transactional outbox (gravar a mensagem em uma tabela na mesma transação do banco e publicá-la a partir dali) ou aceitar uma pequena janela de inconsistência com consumidores idempotentes.
O JTA define o nível de isolamento?
Não. O JTA controla limites, propagação e coordenação, não o isolamento. O isolamento é uma propriedade da conexão, definida no data source na configuração do servidor ou por conexão via JDBC, e o padrão varia conforme o banco: READ COMMITTED no PostgreSQL e no Oracle, REPEATABLE READ no InnoDB do MySQL.