Java EE / Jakarta EE
Jakarta Transactions (JTA 2): transacciones gestionadas por contenedor, propagación y XA
Intermedio11 min de lecturaActualizado
Esta página se tradujo del inglés con ayuda de IA y se cotejó con el original. Leer la versión en inglés
Jakarta Transactions (JTA) es el servicio que permite al contenedor iniciar, confirmar y deshacer trabajo que abarca una base de datos, un broker de mensajes y cualquier otro recurso enlistado. Al terminar esta guía sabrás qué garantiza una transacción y qué dejan abierto los niveles de aislamiento, cómo se comporta cada atributo de propagación, qué excepciones provocan rollback y por qué, cuándo usar transacciones gestionadas por el bean y qué cuesta el commit en dos fases de XA, además de los errores de autoinvocación y de transacciones largas que lo echan todo a perder.
Lo que aprenderás
- Explicar las propiedades ACID, los niveles de aislamiento y las anomalías que permite cada uno
- Aplicar @Transactional y @TransactionAttribute con el tipo de propagación adecuado
- Predecir cuándo hace rollback el contenedor: excepciones de sistema frente a excepciones de aplicación, rollbackOn y setRollbackOnly
- Usar UserTransaction para transacciones gestionadas por el bean cuando los límites declarativos no encajan
- Decidir cuándo es necesario el commit en dos fases de XA y qué alternativas existen
Una transacción es una promesa que la plataforma hace en tu nombre: todo lo que está dentro del límite ocurre por completo o no ocurre, y nadie más ve el estado a medio hacer. En Jakarta EE esa promesa la cumple el servicio Jakarta Transactions —al que todo el mundo sigue llamando JTA—, que el contenedor aplica alrededor de cada método de un enterprise bean y de cada método CDI @Transactional, y que además coordina los recursos JPA, JMS y JDBC para que un único commit los cubra a todos. Los desarrolladores rara vez llaman a JTA directamente: lo configuran con anotaciones y se topan con sus reglas la primera vez que una excepción no provoca rollback, que una llamada anidada no obtiene su propia transacción o que se envía un mensaje de un pedido que nunca llegó a guardarse. Esta guía explica esas reglas lo bastante bien como para que puedas anticipar esos casos.
Qué garantiza una transacción
Las propiedades ACID describen el contrato:
- Atomicidad: todas las operaciones de la transacción surten efecto, o no surte efecto ninguna.
- Consistencia: la transacción lleva la base de datos de un estado válido a otro; las restricciones se cumplen en el commit.
- Aislamiento: las transacciones concurrentes no ven el estado intermedio de las demás (en un grado que depende del nivel de aislamiento).
- Durabilidad: una vez confirmados, los cambios sobreviven a una caída.
El aislamiento es la propiedad negociable, porque el aislamiento total se paga en rendimiento. El estándar SQL define cuatro niveles según las anomalías que permiten:
| Nivel | Lectura sucia | Lectura no repetible | Lectura fantasma | Coste típico |
|---|---|---|---|---|
READ UNCOMMITTED | posible | posible | posible | el más bajo; rara vez se usa |
READ COMMITTED | evitada | posible | posible | por defecto en PostgreSQL, Oracle, SQL Server |
REPEATABLE READ | evitada | evitada | posible (evitada en algunos motores) | por defecto en MySQL InnoDB |
SERIALIZABLE | evitada | evitada | evitada | el más alto; reintentos ante conflictos |
Una lectura sucia (dirty read) ve datos no confirmados de otra transacción; una lectura no repetible obtiene un valor distinto al volver a leer la misma fila; una lectura fantasma ve aparecer filas nuevas al repetir una consulta por rango. READ COMMITTED junto con bloqueo optimista (@Version en JPA) es la combinación sobre la que funcionan la mayoría de las aplicaciones de negocio: las lecturas son baratas y las actualizaciones perdidas se detectan en el momento de escribir en lugar de evitarse con bloqueos. Ten en cuenta que JTA no fija el nivel por sí mismo: configúralo en el data source.
Transacciones locales frente a globales
Una transacción local implica un único recurso: el commit() de una sola conexión JDBC. La base de datos hace todo el trabajo y JTA solo le indica cuándo. Una transacción global (distribuida) abarca varios recursos —dos bases de datos, o una base de datos y un broker de mensajes— y necesita un coordinador capaz de hacer que todos confirmen o todos deshagan. JTA proporciona ese coordinador, el transaction manager, y los recursos participan a través de la interfaz XA (javax.transaction.xa.XAResource, que se quedó en el espacio de nombres de Java SE).
En un servidor, los recursos se declaran como data sources y connection factories XA o no XA. Enlistar un recurso no XA en una transacción junto con otro recurso o bien se rechaza, o bien se resuelve con una «last resource commit optimisation» propia del fabricante, que no es del todo segura. La mayoría de las aplicaciones usan una sola base de datos y salen ganando con un data source no XA: es más simple, más rápido, y el transaction manager te sigue dando límites declarativos y rollback.
Transacciones gestionadas por el contenedor
El contenedor delimita la transacción alrededor de una llamada a un método y decide, a partir de una anotación, cómo se relaciona el método con cualquier transacción que ya esté en curso en el llamador. Los seis tipos de propagación son los mismos para @TransactionAttribute en los enterprise beans y para @Transactional en los beans CDI:
| Tipo | El llamador tiene una transacción | El llamador no tiene ninguna |
|---|---|---|
REQUIRED (por defecto) | Unirse a ella | Iniciar una nueva y hacer commit al retornar |
REQUIRES_NEW | Suspenderla, ejecutarse en una nueva y reanudarla después | Iniciar una nueva |
MANDATORY | Unirse a ella | Lanzar TransactionalException / EJBTransactionRequiredException |
SUPPORTS | Unirse a ella | Ejecutarse sin transacción |
NOT_SUPPORTED | Suspenderla y ejecutarse sin ella | Ejecutarse sin ella |
NEVER | Lanzar una excepción | Ejecutarse sin ella |
REQUIRED es lo correcto para casi cualquier método de negocio: la llamada más externa es dueña de la transacción y las internas se unen a ella, de modo que un servicio que llama a dos repositorios obtiene una única unidad atómica. REQUIRES_NEW es para el trabajo que debe confirmarse aunque el llamador falle —un registro de auditoría, un estado de «procesamiento fallido»— y cuesta una segunda conexión y, con XA, una segunda rama. MANDATORY documenta que un método nunca es un punto de entrada. NOT_SUPPORTED y SUPPORTS son para rutas de solo lectura que no deben mantener una transacción, como las consultas largas de informes; NEVER protege frente a que lo llamen desde dentro de una.
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 que esto funcione como se espera, PaymentDeclinedException debe provocar el rollback de la transacción externa. Lo que nos lleva a las reglas.
Qué provoca rollback
Ambas anotaciones distinguen dos tipos de fallo. Una excepción de sistema —cualquier RuntimeException o Error— se trata como un bug o un fallo de infraestructura: el contenedor marca la transacción para rollback y, en los enterprise beans, la envuelve en una EJBException y descarta la instancia. Una excepción de aplicación —una excepción checked— se trata como un resultado de negocio: la transacción hace commit y la excepción se propaga sin cambios.
Puedes cambiar el comportamiento por defecto en ambos sentidos:
@Transactional(rollbackOn = PaymentDeclinedException.class)hace rollback ante esa excepción checked;dontRollbackOn = ValidationException.classhace commit a pesar de esa excepción runtime.- En los enterprise beans, anota la clase de la excepción con
@ApplicationException(rollback = true); eso además evita que una excepción runtime quede envuelta en unaEJBException. - Por programación, inyecta
TransactionManageroEJBContexty llama asetRollbackOnly(). La transacción continúa, pero ya no puede confirmarse —se lanza unaRollbackExceptionen el límite—, que es lo que debe hacer un paso de validación enterrado en la cadena de llamadas cuando no puede lanzar una excepción.
Una vez que una transacción está marcada para rollback, nada puede desmarcarla. Una sorpresa habitual: un método interno REQUIRED lanza una excepción runtime, el método externo la captura y sigue adelante, y el commit externo falla con RollbackException, porque el fallo interno ya había condenado la transacción compartida.
Timeouts
Todos los servidores tienen un timeout de transacción por defecto (habitualmente 300 segundos); cuando vence, un thread «reaper» hace rollback de la transacción y el siguiente acceso a un recurso falla. Fija límites más cortos en los métodos de larga duración con anotaciones propias del fabricante (@TransactionTimeout en WildFly) o llamando a UserTransaction.setTransactionTimeout en código gestionado por el bean. Una transacción que necesita minutos suele indicar un proceso por lotes que habría que trocear.
Transacciones gestionadas por el bean
Cuando los límites no coinciden con las llamadas a métodos —un bucle de batch que hace commit cada mil registros, un método que debe decidir en tiempo de ejecución si inicia una transacción—, toma el control con 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 está disponible para cualquier componente mediante @Resource o el nombre JNDI java:comp/UserTransaction; los beans CDI también pueden inyectarla. Un bean stateless con transacciones gestionadas por el bean no debe retornar con una transacción todavía abierta: el contenedor la deshace y registra un error. El EntityManager con ámbito de transacción se une a la transacción que esté activa en el momento de usarlo, así que un em.clear() después de cada commit evita que el contexto de persistencia crezca.
TransactionSynchronizationRegistry es la tercera API que conviene conocer: permite registrar un callback para beforeCompletion y afterCompletion, que es la forma de enviar una notificación solo cuando el commit ha tenido éxito de verdad. Los observers transaccionales de CDI 4.0 consiguen lo mismo de forma declarativa: void onOrderPaid(@Observes(during = TransactionPhase.AFTER_SUCCESS) OrderPaid event).
XA y commit en dos fases
Cuando una transacción ha enlistado dos recursos XA —por ejemplo, un data source XA de PostgreSQL y una connection factory de ActiveMQ Artemis—, el commit se ejecuta en dos fases:
- Preparación. El transaction manager pide a cada recurso que se prepare: que escriba en almacenamiento duradero todo lo necesario para confirmar y que vote sí o no. Un recurso que vota sí debe poder confirmar más adelante, incluso después de una caída.
- Commit. Si todos votaron sí, el manager escribe un registro de decisión en su propio log de transacciones y ordena a cada recurso que confirme; si no, les ordena a todos que deshagan.
El log es lo que hace recuperable el protocolo: si el servidor cae entre las dos fases, al reiniciar el manager lee el log y completa las ramas en duda (in-doubt). Ese es también el coste operativo de XA: el log debe estar en un almacenamiento fiable, debe sobrevivir a los redespliegues y a los reinicios del contenedor, y un recurso que sigue inaccesible deja transacciones in-doubt que retienen bloqueos hasta que termina la recuperación o un administrador las resuelve. El rendimiento es menor por las idas y vueltas adicionales y las escrituras forzadas a disco.
Antes de elegir XA, considera las alternativas:
- Un solo recurso. La mayoría de los servicios necesitan una sola base de datos. Guarda los mensajes en una tabla (transactional outbox) y deja que un relay los publique; la transacción local cubre ambas cosas.
- Consumidores idempotentes. Envía el mensaje después del commit y haz que los consumidores toleren un duplicado o un mensaje que hace referencia a un registro que todavía no existe.
- Compensación. En procesos de negocio largos que abarcan varios servicios, las sagas sustituyen la atomicidad por acciones de compensación explícitas; la guía de transacciones distribuidas analiza los compromisos.
XA es la elección correcta cuando los dos recursos son realmente críticos, el volumen es moderado y existe la madurez operativa para vigilar la recuperación; el procesamiento clásico de message-driven bean más base de datos es exactamente ese caso, y el contenedor lo reduce a un cambio de configuración de una línea.
Cómo encajan las piezas en tiempo de ejecución
Considera un método de un session bean con REQUIRED que persiste un pedido mediante JPA y envía un mensaje JMS a través de un JMSContext inyectado:
- El proxy pide al transaction manager que inicie una transacción y la asocia al thread actual.
- El primer uso del
EntityManagerobtiene una conexión del data source; la conexión queda enlistada. - El envío con
JMSContextenlista la sesión JMS; el mensaje queda en un búfer, no se entrega. - El método retorna con normalidad. El contenedor llama a la sincronización
beforeCompletiondel proveedor JPA, que hace flush del contexto de persistencia: elINSERTse ejecuta ahora, y las violaciones de restricciones aparecen aquí comoPersistenceException. - El manager hace commit: en una fase si solo hay un recurso enlistado, en dos fases en caso contrario. El mensaje solo es visible para los consumidores después del commit.
- El contexto de persistencia se cierra; las entidades pasan a estar detached.
El paso 4 explica por qué las excepciones de la base de datos aparecen «después» de que tu código haya terminado, y por qué un método @Transactional no puede capturar sus propios fallos de flush: el flush ocurre fuera del cuerpo del método. Si necesitas reaccionar ante una violación de restricción, llama explícitamente a em.flush() dentro del método y captura la excepción ahí.
Errores comunes
- Autoinvocación. Llamar a
this.record(...)sobre un métodoREQUIRES_NEWlo ejecuta en la transacción del llamador, porque se salta el proxy. Inyecta el bean en sí mismo o mueve el método a otro bean. - Esperar que las excepciones checked provoquen rollback. Por defecto hacen commit. Usa
rollbackOno@ApplicationException(rollback = true). - Capturar la excepción interna y hacer commit de todos modos. La transacción ya está marcada; el commit lanza
RollbackException. CompruebagetStatus()(ogetRollbackOnly()en el contexto del enterprise bean) antes de continuar, o usaREQUIRES_NEWpara el trabajo interno. - Transacciones largas. Mantener una transacción abierta durante una llamada HTTP remota o mientras el usuario piensa mantiene ocupados bloqueos y conexiones. Ciñe el límite al acceso a datos.
- Efectos secundarios externos dentro de la transacción. Un correo enviado antes de un rollback no se puede anular. Envíalo desde un observer
AFTER_SUCCESSo mediante un outbox. - XA por defecto. Configurar todos los data sources como XA cuesta rendimiento y añade una recuperación que operar, en aplicaciones que nunca enlistan un segundo recurso.
@Transactionalen un método privado o en un objeto que no es CDI. Sin proxy no hay interceptor, y sin interceptor no hay transacción. Solo métodos públicos en beans gestionados.
Puntos clave
- JTA da al contenedor un transaction manager que delimita el trabajo y coordina los recursos enlistados; ACID es el contrato, y el nivel de aislamiento se configura en el data source, no en JTA.
REQUIREDpara los métodos de negocio,REQUIRES_NEWpara el trabajo que debe sobrevivir al fallo del llamador,MANDATORYyNEVERcomo salvaguardas,SUPPORTS/NOT_SUPPORTEDpara rutas de solo lectura.- Las excepciones runtime provocan rollback y las checked hacen commit; cámbialo con
rollbackOn,@ApplicationExceptionosetRollbackOnly. - El flush ocurre en el commit, fuera de tu método; llama a
em.flush()explícitamente si necesitas tratar los errores de base de datos en el sitio. - Usa
UserTransactionpara batches troceados y límites decididos en tiempo de ejecución; nunca dejes una transacción abierta cuando un bean stateless retorna. - El commit en dos fases de XA es recuperable y correcto, pero cuesta rendimiento y operación; prefiere un solo recurso más un outbox salvo que ambos recursos sean críticos.
- Las llamadas a través de
thisse saltan el proxy y, con él, todos los atributos transaccionales.
Fuentes y lecturas recomendadas
Preguntas frecuentes
- ¿Qué diferencia hay entre @Transactional y @TransactionAttribute?
- Configuran el mismo servicio del contenedor. @TransactionAttribute (jakarta.ejb) se aplica a los enterprise beans, cuyos métodos son transaccionales por defecto. @Transactional (jakarta.transaction, desde JTA 1.2) es un interceptor binding de CDI que funciona en cualquier bean gestionado, hay que añadirlo explícitamente y aporta los atributos rollbackOn y dontRollbackOn. El código nuevo sobre beans CDI usa @Transactional; los tipos de propagación son idénticos.
- ¿Por qué no se hizo rollback de mi transacción cuando lancé una excepción?
- Porque era una excepción checked. Por defecto, ambas anotaciones hacen rollback ante RuntimeException y Error, y hacen commit ante las excepciones checked. Declara @Transactional(rollbackOn = MyCheckedException.class), marca la clase de la excepción con @ApplicationException(rollback = true) en los enterprise beans o llama a setRollbackOnly antes de retornar.
- ¿Necesito XA si uso una sola base de datos y un solo broker de mensajes?
- Si la escritura en la base de datos y el mensaje deben ser atómicos, sí: son exactamente dos recursos, y solo un commit en dos fases hace que ambos tengan éxito o ambos fallen. Las alternativas son el patrón transactional outbox (escribir el mensaje en una tabla dentro de la misma transacción de base de datos y publicarlo desde ahí) o aceptar una pequeña ventana de inconsistencia con consumidores idempotentes.
- ¿JTA establece el nivel de aislamiento?
- No. JTA controla los límites, la propagación y la coordinación, no el aislamiento. El aislamiento es una propiedad de la conexión que se configura en el data source del servidor o por conexión mediante JDBC, y su valor por defecto depende de la base de datos: READ COMMITTED en PostgreSQL y Oracle, REPEATABLE READ en InnoDB de MySQL.