CodeGym /Cursos /Módulo 5. Spring /Optimización de transacciones en la aplicación

Optimización de transacciones en la aplicación

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

Cuando alguien te dice: «En nuestra aplicación las transacciones van fatal» — eso suele ser solo la punta del iceberg. El problema puede estar en cualquier sitio: desde bloqueos poco optimizados hasta transacciones demasiado largas. El uso incorrecto de las transacciones puede crear cuellos de botella en el rendimiento, convirtiendo una aplicación rápida en una tortuga lenta.

La regla principal para optimizar transacciones es que deben ser lo más cortas y enfocadas posible. Cuantas más operaciones intentes meter en una sola transacción, más tiempo mantendrá los bloqueos y mayor será la probabilidad de conflictos con otras transacciones. ¿Recuerdas el principio KISS? En el mundo de las transacciones funciona al 100%.


Buenas prácticas para gestionar transacciones

Reducir la duración de las transacciones

Cuanto más rápido termine una transacción, menos probabilidad hay de que se encuentre con bloqueos o cree un cuello de botella. Recomendamos:

  • Sacar todas las operaciones que no estén relacionadas con la base de datos (por ejemplo, validaciones o llamadas a servicios externos) fuera de la transacción.
  • Reducir la cantidad de registros que necesitas actualizar o eliminar dentro de una misma transacción.

Ejemplo:


@Transactional
public void processOrder(Order order) {
    validateOrder(order); // Mejor ejecutarlo fuera de la transacción
    updateOrderStatus(order); // La transacción debe centrarse solo en cambiar datos
}

Usar el nivel de aislamiento correcto

Spring soporta distintos niveles de aislamiento de transacción a través del parámetro isolation de la anotación @Transactional. Si no necesitas el nivel más alto de aislamiento, no lo uses a lo loco. Por ejemplo, el nivel READ_COMMITTED — es el punto medio para la mayoría de los casos.

Ejemplo:


@Transactional(isolation = Isolation.READ_COMMITTED)
public Order getOrderById(Long id) {
    return orderRepository.findById(id);
}

Elegir bien el límite de la transacción

Procura no hacer la transacción global para toda la cadena de llamadas. Envuelve en transacción solo los métodos que realmente cambian el estado de los datos.

Mal:


@Transactional
public void processOrder(Order order) {
    validateOrder(order);
    checkInventory(order);
    updateOrderStatus(order);
}

Mejor:


public void processOrder(Order order) {
    validateOrder(order);
    checkInventory(order);
    updateOrderStatusWithTransaction(order); // Transacción solo alrededor de esta parte
}

@Transactional
private void updateOrderStatusWithTransaction(Order order) {
    orderRepository.updateStatus(order);
}

Cambios innecesarios y bloqueos

Minimiza la cantidad de actualizaciones

Cada vez que haces cambios en la base, puede activarse un bloqueo sobre la fila de datos. Eso puede causar conflictos entre transacciones concurrentes. Para evitarlo:

  • Actualiza los datos solo cuando sea realmente necesario.
  • Comprueba si los datos han cambiado antes de enviar la consulta SQL.

Ejemplo:


@Transactional
public void updateUserProfile(User user) {
    User existingUser = userRepository.findById(user.getId());
    if (!existingUser.equals(user)) { // ¿Han cambiado los datos?
        userRepository.save(user); // Enviamos la consulta solo si hace falta
    }
}

Evita las consultas largas

Cuanto más larga sea la consulta, más tiempo durará el bloqueo. Usa paginación o selección con límite (LIMIT) para trabajar con grandes volúmenes de datos.

Ejemplo:


@Transactional(readOnly = true)
public List<Order> fetchLargeOrderBatch() {
    return orderRepository.findAll(PageRequest.of(0, 50)); // Paginación de 50 registros
}

Herramientas para monitorizar y perfilar transacciones

Usar Spring Actuator

Spring Actuator ofrece métricas útiles para monitorizar la aplicación, incluida información sobre transacciones. Conecta Actuator y accede a las métricas a través de los endpoints /actuator/.

Añade la dependencia en pom.xml:


<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

Rastreo de consultas lentas

Activa el logging de consultas lentas en la base de datos. Por ejemplo, con Hibernate puedes hacerlo mediante los parámetros hibernate.show_sql y hibernate.format_sql, para ver todas las consultas en los logs.

Ejemplo de application.properties:


logging.level.org.hibernate.SQL=DEBUG
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true

Uso de transacciones readOnly

A menudo solo lees datos, pero la transacción igual bloquea filas. Para evitar bloqueos innecesarios, usa el atributo readOnly = true. Esto le indica a Hibernate y a la base que no se esperan cambios en los datos.

Ejemplo:


@Transactional(readOnly = true)
public List<Order> getAllOrders() {
    return orderRepository.findAll();
}

Casos de optimización de transacciones

Imagina que trabajas en un sistema de pagos. Una transacción mal pensada que procese 1000 transferencias a la vez puede llevar a bloqueos de tablas o incluso fallos. ¿La solución? Usa procesamiento por lotes (batch) para dividir esas operaciones en trozos más pequeños.

Ejemplo:


@Transactional
public void processPayments(List<Payment> payments) {
    for (Payment payment : payments) {
        paymentRepository.save(payment);
    }
}

Este código no es óptimo. Mejor dividir en lotes y reducir el número de transacciones:


public void processPaymentsInBatches(List<Payment> payments) {
    List<List<Payment>> batches = splitIntoBatches(payments, 100);
    for (List<Payment> batch : batches) {
        processBatch(batch); // envolvemos solo el lote en la transacción
    }
}

@Transactional
private void processBatch(List<Payment> batch) {
    paymentRepository.saveAll(batch);
}

Riesgos y efectos secundarios

Como desarrolladores, siempre buscamos mejorar el rendimiento, pero recuerda: la optimización no es un fin en sí misma. Cambiar niveles de aislamiento a lo loco, reducir demasiado la duración de la transacción o validar poco los datos puede provocar inconsistencia. Por eso siempre:

  • Prueba los cambios antes y después de la optimización.
  • Usa profiling con la base de datos real.
  • Observa cómo se comportan las transacciones bajo alta carga.

Y ya está, has repasado los aspectos clave para optimizar transacciones. Con estos conocimientos podrás ahorrar no solo recursos de tu sistema, sino también los nervios de tus usuarios. Y quizá los tuyos propios. Porque a nadie le gustan los mensajes misteriosos en los logs como "Deadlock detected".

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