CodeGym /Cursos /Módulo 5. Spring /Transacciones en sistemas distribuidos: commit de dos fas...

Transacciones en sistemas distribuidos: commit de dos fases (2PC)

Módulo 5. Spring
Nivel 6 , Lección 6
Disponible

Imagina la situación: tienes un pedido en una tienda online que debe, al mismo tiempo:

  1. Reducir la cantidad del producto en el almacén.
  2. Cobrar el importe de la tarjeta bancaria del cliente.

Esas operaciones ocurren en dos sistemas distintos, por ejemplo, una base de datos gestiona el almacén y otra procesa las operaciones bancarias. ¿Cómo estar seguros de que ambas operaciones se completaron con éxito? ¿Y si una de ellas falla? Ahí es donde entra el commit de dos fases.

Commit de dos fases (2PC) — es un protocolo para garantizar la consistencia de datos en transacciones que van más allá de un único sistema. Coordina a los participantes (por ejemplo, bases de datos o microservicios) para que todos confirmen la operación con éxito o retrocedan (rollback) en caso de error.


Las dos fases del two-phase commit

Como su nombre indica, el proceso tiene dos fases:

  1. Prepare Phase (Fase de preparación):

    • El coordinador de la transacción envía a todos los participantes (por ejemplo, bases de datos o microservicios) una petición para prepararse a ejecutar la transacción.
    • Cada participante realiza acciones preparatorias (por ejemplo, reserva recursos) y responde al coordinador:
      • OK, si está listo para ejecutar la operación.
      • FAIL, si no es posible ejecutar la operación.
  2. Commit Phase (Fase de confirmación):

    • Si TODOS los participantes respondieron OK, el coordinador da la orden de "confirmar" la transacción.
    • Si al menos un participante respondió FAIL, el coordinador da la orden de "hacer rollback" de la transacción.

Aquí tienes un esquema breve:


Coordinador -----> Participantes: "¿Estáis listos?"
Participantes -------> Coordinador: "Listos/No listos"
Coordinador -----> Participantes: "Confirmad/Deshaced"

Ventajas y limitaciones del commit de dos fases

Veamos qué es lo bueno y lo malo de este enfoque.

Ventajas

  1. Consistencia: el protocolo garantiza que todos los participantes estén en un estado sincronizado — o todos confirman los cambios, o todos los revierten.
  2. Modelo simple: 2PC ofrece una forma sencilla de gestionar transacciones en sistemas distribuidos.

Limitaciones

  1. Rendimiento: la fase de preparación añade latencias de red. En sistemas grandes esto puede convertirse en un cuello de botella.
  2. Bloqueo de recursos: durante la transacción los recursos de los participantes pueden quedar bloqueados, lo que reduce el rendimiento global del sistema.
  3. Fallo del coordinador: si el coordinador cae, la transacción puede quedar colgada. Sí, suena feo.

Implementación del two-phase commit en Spring

Spring soporta el commit de dos fases a través de Java Transaction API (JTA). Veamos cómo funciona.

Configuración de JTA en Spring Boot

Para trabajar con commit de dos fases necesitaremos JTA y soporte para múltiples DataSources. Como ejemplo usaremos dos bases de datos: una para gestionar pedidos y otra para gestionar pagos.

Añadamos las dependencias JTA en pom.xml


<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
<dependency>
  <groupId>com.atomikos</groupId>
  <artifactId>transactions-jta</artifactId>
  <version>5.0.8</version>
</dependency>

Atomikos — es un transaction manager popular que soporta JTA. Asumirá el rol de coordinador.

Configuramos dos DataSources


@Configuration
public class DataSourceConfig {

    @Bean(name = "ordersDataSource")
    @Primary
    public DataSource ordersDataSource() {
        AtomikosDataSourceBean dataSource = new AtomikosDataSourceBean();
        dataSource.setUniqueResourceName("ordersDB");
        dataSource.setXaDataSourceClassName("com.mysql.cj.jdbc.MysqlXADataSource");
        dataSource.setXaProperties(createDatabaseProperties("jdbc:mysql://localhost:3306/orders", "root", "password"));
        return dataSource;
    }

    @Bean(name = "paymentsDataSource")
    public DataSource paymentsDataSource() {
        AtomikosDataSourceBean dataSource = new AtomikosDataSourceBean();
        dataSource.setUniqueResourceName("paymentsDB");
        dataSource.setXaDataSourceClassName("com.mysql.cj.jdbc.MysqlXADataSource");
        dataSource.setXaProperties(createDatabaseProperties("jdbc:mysql://localhost:3306/payments", "root", "password"));
        return dataSource;
    }

    private Properties createDatabaseProperties(String url, String username, String password) {
        Properties properties = new Properties();
        properties.setProperty("user", username);
        properties.setProperty("password", password);
        properties.setProperty("url", url);
        return properties;
    }
}

Definimos el TransactionManager

Consejo:

Spring detecta automáticamente las bases de datos JTA si están configuradas correctamente.


Ejemplo de método transaccional con two-phase

Ahora que tenemos dos DataSources, podemos escribir métodos transaccionales que trabajen con ambos a la vez.


@Service
public class OrderService {

    @Autowired
    private OrderRepository orderRepository;

    @Autowired
    private PaymentRepository paymentRepository;

    @Transactional
    public void createOrderAndProcessPayment(Order order, Payment payment) {
        // Guardamos el pedido en una base de datos
        orderRepository.save(order);

        // Procesamos el pago en otra base de datos
        paymentRepository.save(payment);

        // Si algo va mal, la transacción se revertirá automáticamente
        if (payment.getAmount() > 1000) {
            throw new IllegalArgumentException("¡Importe demasiado grande!");
        }
    }
}

¿Cuándo usar el commit de dos fases?

El commit de dos fases funciona bien cuando:

  • Tienes requisitos estrictos de consistencia de datos.
  • Las transacciones afectan a varios sistemas o bases de datos.

No obstante, ten en cuenta sus limitaciones: coste elevado y vulnerabilidad a fallos. Si la consistencia no es tan crítica, puedes considerar alternativas más ligeras, como la arquitectura orientada a eventos (EDA) — de esto hablaremos más adelante en el curso.


Errores típicos y cómo evitarlos

El error más común es no contemplar la posibilidad de fallo del coordinador. En ese caso la transacción puede quedar en estado pendiente y los recursos bloqueados causarán problemas.

Para evitarlo, es importante:

  • Usar transaction managers fiables (por ejemplo, Atomikos).
  • Monitorizar el estado de las transacciones y resolver las que queden colgadas a tiempo.

Con esto cerramos la inmersión en el commit de dos fases. En la próxima clase discutiremos cómo optimizar transacciones en aplicaciones sin sacrificar rendimiento.

Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION