Retour à l'accueil

Optimisation du code legacy : réduction du temps de traitement de 99 %

Décomposition d'un cas réel d'optimisation du traitement de 75 mille enregistrements dans un système legacy. Montre comment en 4 étapes réduire le temps d'exécution de 105 secondes à 1.05 secondes grâce à l'élimination des opérations redondantes, la configuration du traitement par lots et la mise à l'échelle sécurisée.

Cas réel : optimisation de code legacy de 105 sec à 1.05 sec
Advertisement 728x90

Optimisation de code legacy : Comment réduire le temps de traitement de 99 % dans un cas concret

L'implémentation d'une intégration REST avec un système externe rencontre souvent des problèmes de performances. Dans cet article, nous décomposons comment nous avons accéléré le traitement de 75 000 enregistrements en seulement 4 étapes, en réduisant le temps d'exécution de 99 %. Le cas est basé sur une tâche réelle d'intégration d'un système bancaire avec un partenaire externe sous des contraintes strictes.

Problème : Intégration REST avec contraintes du partenaire

Conditions initiales :

  • Besoin d'envoyer les données utilisateur vers le système du partenaire via REST API
  • Le partenaire n'accepte que des requêtes unitaires (pas de traitement par lots autorisé)
  • Limite de temps d'exécution : une fenêtre de 5 h du matin pour minimiser la charge
  • Volume de données : 75 000 enregistrements avec statut NEW
  • Pas de traitement asynchrone ni de mise à l'échelle horizontale autorisés

Le code original utilisait un chargement paginé via Spring Data Pageable avec une taille de page de 2000 enregistrements. L'erreur critique consistait à accumuler toutes les entités modifiées en mémoire jusqu'à une unique opération saveAll(), ce qui entraînait :

Google AdInline article slot
  • Vidage automatique du contexte avant chaque SELECT
  • Génération de 2000 requêtes UPDATE par page
  • Verrou de transaction pendant les appels HTTP

Les benchmarks initiaux montraient un temps de traitement ≈105 secondes sur un Apple M4 avec 16 GB RAM. L'émulation d'un délai de 1 ms du système externe rendait cette métrique critique pour la production.

Étape 1 : Élimination des opérations DB redondantes

L'analyse des logs SQL a révélé le défaut fatal : Hibernate générait 2000 UPDATE distincts au lieu d'opérations par lots. La cause était la conservation des entités dans le contexte de persistance à travers les itérations de boucle. La solution impliquait deux changements clés :

  • Remplacement des sauvegardes en fin de processus par des commits intermédiaires :
content.forEach(this::registerUser);
userRepository.saveAll(content);
  • Implémentation d'une requête native pour les mises à jour de statut en lot :
@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);

Ces changements ont donné des résultats initiaux — une réduction de 6 % du temps (à 98,4 secondes). Enseignement principal : Les méthodes standard de Spring Data JPA peuvent entraîner un surcoût caché avec de grands volumes de données.

Google AdInline article slot

Étape 2 : Ajustement fin des paramètres de traitement

La prochaine optimisation s'est concentrée sur la taille des lots. Les tests ont montré :

  • À taille 500 : temps augmenté de 8,41 % (106,68 s)
  • À taille 2000 : référence 98,4 s
  • À taille 5000 : amélioration de 1,86 % (96,58 s)

Il est important de comprendre qu'il n'y a pas de solution miracle. La taille optimale dépend de :

  • Caractéristiques du matériel (IOPS disque, bande passante réseau)
  • Complexité de la logique métier dans la boucle de traitement
  • Paramètres du pool de connexions DB

Approche recommandée : Effectuer des tests A/B dans un environnement proche de la production avec profilage des ressources (CPU, mémoire, E/S disque). Dans notre cas, 5000 enregistrements se sont révélés optimaux, réduisant le nombre de commits de transaction.

Google AdInline article slot

Étape 3 : Mise à l'échelle horizontale sécurisée

La tentative de traitement à deux threads a tourné au désastre — duplication de toutes les opérations (150 000 au lieu de 75 000 requêtes). La solution nécessitait un changement de stratégie d'accès aux données :

  • Implémentation de SELECT FOR UPDATE SKIP LOCKED :
SELECT * FROM users WHERE status = 'NEW' ORDER BY id FOR UPDATE SKIP LOCKED LIMIT 5000
  • Implémentation d'une boucle transactionnelle :
@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()
  );
}

Cette approche garantissait :

  • Un traitement unique assuré
  • Verrous de table minimaux
  • Possibilité d'exécuter N instances parallèles

Après implémentation, le temps de traitement est tombé à 1,05 seconde — une réduction de 99 % par rapport à l'original.

Étapes clés d'optimisation

  • Analyse des surcoûts cachés

- Activation de show-sql pour suivre les requêtes générées

- Vérification du comportement du contexte de persistance dans les boucles

  • Passage aux opérations par lots

- Remplacement de saveAll() par des commits intermédiaires

- Utilisation de requêtes natives pour les mises à jour en masse

  • Expérimentation avec les paramètres

- Tests de diverses tailles de lots

- Surveillance des métriques système pendant les tests

  • Préparation à la mise à l'échelle

- Implémentation de SKIP LOCKED pour l'accès concurrent

- Décomposition des transactions en blocs atomiques

Enseignements principaux

  • Vidage Hibernate avant SELECT — un problème critique avec de grands volumes de données. Chaque requête DB déclenche un vidage automatique, provoquant une cascade d'UPDATE.
  • La taille de lot n'est pas constante. Nécessite un réglage empirique basé sur les spécificités de la charge. Les tests ont montré 5000 enregistrements comme optimaux.
  • Isolation transactionnelle pour la mise à l'échelle. SELECT FOR UPDATE SKIP LOCKED permet un traitement concurrent sans duplication mais augmente les temps de verrou.
  • Le profilage est essentiel. Sans benchmarks proches de la production, les optimisations peuvent avoir l'effet inverse (comme réduire la taille de lot à 500).
  • Compatibilité avec les contraintes legacy. Les solutions doivent tenir compte des limites externes (par ex., interdiction de lots par le partenaire), excluant un simple traitement asynchrone.

L'implémentation finale a livré des temps de traitement stables de 1,05 ± 0,05 seconde en test de charge avec 100 000 enregistrements. La clé du succès a été la combinaison d'optimisations techniques avec une compréhension approfondie du contrat du système externe. Cette approche fonctionne pour tout scénario d'intégration REST API avec limites de taille de requête et exigences de fiabilité.

— Editorial Team

Advertisement 728x90

Lire ensuite