Transacciones en JDBC

commit, rollback y niveles de aislamiento

Commit y rollback

Por defecto JDBC tiene autoCommit=true: cada sentencia es una transacción independiente. Para operaciones que deben ser atómicas (todo o nada), se desactiva el autocommit y se controla manualmente el commit y el rollback. El patrón estándar es: desactivar autocommit → ejecutar operaciones → commit en éxito, rollback en fallo.

import java.sql.*;

// Transacción manual: desactivar autocommit y controlar commit/rollback
public void transferir(long cuentaOrigen, long cuentaDestino, double importe)
        throws SQLException {
    try (Connection con = dataSource.getConnection()) {
        con.setAutoCommit(false); // inicio de la transacción
        try {
            // Las dos operaciones deben ser atómicas
            descontar(con, cuentaOrigen, importe);
            acreditar(con, cuentaDestino, importe);
            con.commit(); // ambas exitosas → confirmar
        } catch (SQLException e) {
            con.rollback(); // cualquier fallo → deshacer todo
            throw e;        // propagar la excepción
        }
    }
}

private void descontar(Connection con, long id, double importe) throws SQLException {
    String sql = "UPDATE cuentas SET saldo = saldo - ? WHERE id = ? AND saldo >= ?";
    try (PreparedStatement ps = con.prepareStatement(sql)) {
        ps.setDouble(1, importe);
        ps.setLong(2, id);
        ps.setDouble(3, importe);
        if (ps.executeUpdate() == 0) throw new SQLException("Saldo insuficiente en cuenta " + id);
    }
}

Savepoints

Un savepoint marca un punto intermedio dentro de una transacción. Se puede hacer rollback hasta el savepoint sin deshacer toda la transacción. Útil para escenarios donde parte del trabajo debe persistir aunque otra parte falle.

// Savepoint: punto de guardado intermedio dentro de una transacción
try (Connection con = dataSource.getConnection()) {
    con.setAutoCommit(false);

    // Primera parte de la transacción
    insertarPedido(con, pedido);
    Savepoint sp = con.setSavepoint("pedido_insertado");

    try {
        // Segunda parte — puede fallar
        reservarStock(con, pedido.getProductos());
        con.commit();
    } catch (StockInsuficienteException e) {
        // Revertir solo desde el savepoint, no toda la transacción
        con.rollback(sp);
        // El pedido sigue insertado, pero sin reserva de stock
        marcarPedidoPendienteStock(con, pedido.getId());
        con.commit();
    }
}

Niveles de aislamiento

Los niveles de aislamiento controlan el equilibrio entre consistencia y rendimiento en entornos con múltiples transacciones concurrentes. Mayor aislamiento = más consistencia, pero más contención de locks y menor throughput.

// Niveles de aislamiento: controlan qué ven las transacciones concurrentes

// Problemas de concurrencia:
// Dirty read:          leer datos no confirmados de otra transacción
// Non-repeatable read: releer la misma fila y obtener valor diferente
// Phantom read:        releer un rango y obtener filas nuevas/desaparecidas

// Niveles (de menor a mayor aislamiento):
con.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);
// Permite dirty reads — prácticamente nunca se usa

con.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
// Nivel estándar en la mayoría de BBDD (Oracle, PostgreSQL por defecto)
// Evita dirty reads; permite non-repeatable reads y phantoms

con.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
// Nivel de MySQL por defecto
// Evita dirty reads y non-repeatable reads; permite phantoms

con.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
// Máximo aislamiento — transacciones completamente serializadas
// Evita todos los problemas; mayor contención y menor rendimiento

Deadlocks

// Deadlock: dos transacciones se bloquean mutuamente esperando recursos del otro
// La BBDD detecta el deadlock y mata una de las transacciones (lanza SQLException)

// Prevención:
// 1. Adquirir locks siempre en el mismo orden (cliente antes que pedido)
// 2. Mantener las transacciones lo más cortas posible
// 3. Usar SELECT FOR UPDATE solo cuando sea necesario

// Reintentar ante deadlock:
public void operacionConReintento(int maxIntentos) throws SQLException {
    for (int intento = 1; intento <= maxIntentos; intento++) {
        try {
            ejecutarTransaccion();
            return; // éxito
        } catch (SQLException e) {
            if (esDeadlock(e) && intento < maxIntentos) {
                System.out.println("Deadlock detectado, reintentando (" + intento + "/" + maxIntentos + ")");
                continue;
            }
            throw e;
        }
    }
}

boolean esDeadlock(SQLException e) {
    return "40001".equals(e.getSQLState()) || e.getMessage().contains("deadlock");
}
En Spring: @Transactional gestiona el commit/rollback automáticamente — hace rollback en RuntimeException y commit en éxito. El nivel de aislamiento se puede configurar con @Transactional(isolation = Isolation.READ_COMMITTED). Entender JDBC manual es la base para diagnosticar problemas de transacciones en Spring.

Siguiente apartado → Serialización y persistencia manual

Índice de la sección

Índice del curso