Volver al inicio

Optimización de código legado: reducción del tiempo de procesamiento en un 99%

Desglose de un caso real de optimización del procesamiento de 75 mil registros en un sistema legado. Muestra cómo en 4 pasos reducir el tiempo de ejecución de 105 segundos a 1.05 segundos mediante la eliminación de operaciones redundantes, configuración de procesamiento por lotes y escalado seguro.

Caso real: optimización de código legado de 105 seg a 1.05 seg
Advertisement 728x90

Optimizando código legado: Cómo reducir el tiempo de procesamiento en un 99 % en un caso real

La implementación de integración REST con un sistema externo suele toparse con problemas de rendimiento. En este artículo, desglosamos cómo aceleramos el procesamiento de 75.000 registros en solo 4 pasos, reduciendo el tiempo de ejecución en un 99 %. El caso se basa en una tarea real de integración de un sistema bancario con un socio externo bajo restricciones estrictas.

Problema: Integración REST con restricciones del socio

Condiciones iniciales:

  • Necesidad de enviar datos de usuario al sistema del socio vía REST API
  • El socio solo acepta solicitudes individuales (no se permite procesamiento por lotes)
  • Límite de tiempo de ejecución: una ventana de las 5 a. m. para minimizar la carga
  • Volumen de datos: 75.000 registros con estado NEW
  • No se permite procesamiento asíncrono ni escalado horizontal

El código original usaba carga paginada vía Spring Data Pageable con un tamaño de página de 2000 registros. El error crítico era acumular todas las entidades modificadas en memoria hasta una única operación saveAll(), lo que provocaba:

Google AdInline article slot
  • Vaciamiento automático del contexto antes de cada SELECT
  • Generación de 2000 consultas UPDATE por página
  • Bloqueo de transacción durante las llamadas HTTP

Los benchmarks iniciales mostraron un tiempo de procesamiento ≈105 segundos en un Apple M4 con 16 GB RAM. Emulando un retraso de 1 ms del sistema externo convirtió esto en una métrica crítica para producción.

Paso 1: Eliminando operaciones redundantes en la BD

El análisis del registro SQL reveló el defecto fatal: Hibernate generaba 2000 UPDATE separadas en lugar de operaciones por lotes. La causa era mantener las entidades en el contexto de persistencia a través de las iteraciones del bucle. La solución implicó dos cambios clave:

  • Reemplazar los guardados al final del proceso por commits intermedios:
content.forEach(this::registerUser);
userRepository.saveAll(content);
  • Implementar una consulta nativa para actualizaciones masivas de estado:
@Modifying
@Query(value = """
UPDATE users SET status = CAST(:status AS text)
WHERE id IN (:ids)
""", nativeQuery = true)
int batchUpdateStatus(@Param("ids") Collection<Long> ids, @Param("status") String status);

Estos cambios arrojaron resultados iniciales: una reducción del 6 % en el tiempo (a 98,4 segundos). Lección clave: los métodos estándar de Spring Data JPA pueden generar sobrecargas ocultas con volúmenes grandes de datos.

Google AdInline article slot

Paso 2: Ajuste fino de parámetros de procesamiento

La siguiente optimización se centró en el tamaño de lote. Las pruebas mostraron:

  • Con tamaño 500: tiempo aumentó un 8,41 % (106,68 s)
  • Con tamaño 2000: línea base 98,4 s
  • Con tamaño 5000: mejora del 1,86 % (96,58 s)

Es importante entender que no hay bala de plata. El tamaño óptimo depende de:

  • Especificaciones de hardware (IOPS de disco, ancho de banda de red)
  • Complejidad de la lógica de negocio en el bucle de procesamiento
  • Configuración del pool de conexiones a BD

Enfoque recomendado: ejecutar pruebas A/B en un entorno similar a producción con perfilado de recursos (CPU, memoria, E/S de disco). En nuestro caso, 5000 registros resultó óptimo, reduciendo el número de commits de transacción.

Google AdInline article slot

Paso 3: Escalado horizontal seguro

Intentar procesamiento con dos hilos llevó al desastre: duplicando todas las operaciones (150.000 en lugar de 75.000 consultas). La solución requirió cambiar la estrategia de acceso a datos:

  • Implementar SELECT FOR UPDATE SKIP LOCKED:
SELECT * FROM users WHERE status = 'NEW' ORDER BY id FOR UPDATE SKIP LOCKED LIMIT 5000
  • Implementar un bucle transaccional:
@Transactional
public void processBatch() {
  List<User> batch = userRepository.lockAndSelectBatch(5000);
  batch.forEach(this::registerUser);
  userRepository.batchUpdateStatus(
    batch.stream().map(User::getId).collect(Collectors.toList()),
    UserStatus.PROCESSED.name()
  );
}

Este enfoque garantizó:

  • Procesamiento único garantizado
  • Bloqueos mínimos de tabla
  • Capacidad para ejecutar N instancias en paralelo

Tras la implementación, el tiempo de procesamiento cayó a 1,05 segundos: una reducción del 99 % respecto al original.

Etapas clave de optimización

  • Analizando sobrecargas ocultas

- Activando show-sql para rastrear consultas generadas

- Verificando el comportamiento del contexto de persistencia en bucles

  • Cambiando a operaciones por lotes

- Reemplazando saveAll() por commits intermedios

- Usando consultas nativas para actualizaciones masivas

  • Experimentando con parámetros

- Probando varios tamaños de lote

- Monitoreando métricas del sistema durante las pruebas

  • Preparando para el escalado

- Implementando SKIP LOCKED para acceso concurrente

- Descomponiendo transacciones en bloques atómicos

Lecciones clave

  • Flush de Hibernate antes de SELECT: un problema crítico con volúmenes grandes de datos. Cada consulta a BD dispara un flush automático, provocando una cascada de UPDATE.
  • El tamaño de lote no es constante. Requiere ajuste empírico basado en las especificidades de la carga de trabajo. Las pruebas mostraron 5000 registros como óptimo.
  • Aislamiento transaccional para escalado. SELECT FOR UPDATE SKIP LOCKED permite procesamiento concurrente sin duplicación, pero aumenta los tiempos de retención de bloqueos.
  • El perfilado es esencial. Sin benchmarks similares a producción, las optimizaciones pueden salir mal (como reducir el tamaño de lote a 500).
  • Compatibilidad con restricciones legacy. Las soluciones deben considerar límites externos (p. ej., prohibición de lotes por parte del socio), descartando procesamiento asíncrono simple.

La implementación final entregó tiempos de procesamiento estables de 1,05 ± 0,05 segundos bajo pruebas de carga con 100.000 registros. La clave del éxito fue combinar optimizaciones técnicas con una profunda comprensión del contrato del sistema externo. Este enfoque funciona para cualquier escenario de integración REST API con límites de tamaño de solicitud y requisitos de fiabilidad.

— Editorial Team

Advertisement 728x90

Leer después