Stratégies d'optimisation des transactions longues avec Spring

Comprendre les transactions longues

Une transaction longue (ou "Big Transaction") désigne une opération dont le temps d'exécution dépasse les seuils recommandés pour maintenir la santé d'une base de données. En Spring, les transactions sont gérées via deux approches : déclarative (@Transactional) ou programmatique (TransactionTemplate).

Facteurs déclencheurs et impacts

Plusieurs éléments contribuetn à l'allongement d'une transaction :

  • Le traitement massif de données en une seule opération.
  • L'inclusion d'appels RPC ou d'API tierces.
  • Des calculs complexes intégrés dans le flux transactionnel.
  • Une contention importante sur les verrous (locks).

Les conséquences sur le système sont critiques : épuisement du pool de connexions, augmentation de la latence de réplication (master-slave), croissance démesurée des journaux d'annulation (undo log) et blocages en cascade impactant la disponibilité globale.

1. Privilégier la gestion programmatique

L'annotation @Transactional est souvent appliquée trop largement au niveau d'une méthode de service complète. Cela englobe souvent du code qui n'a pas besoin d'être transactionnel.

L'utilisation de TransactionTemplate permet de réduire la portée au strict nécessaire :

@Service
public class OrderService {
    @Autowired
    private TransactionTemplate txTemplate;
    @Autowired
    private InventoryMapper inventoryMapper;

    public void processOrder(OrderRequest request) {
        // Opérations hors transaction (ex: validation)
        validateRequest(request);

        txTemplate.execute(status -> {
            try {
                inventoryMapper.reduceStock(request.getProductId(), request.getQuantity());
                inventoryMapper.recordLog(request.getOrderId());
                return true;
            } catch (Exception e) {
                status.setRollbackOnly();
                throw e;
            }
        });
    }
}

2. Exclure les requêtes de lecture (SELECT)

Sauf besoin spécifique d'isolation (comme le verrouillage pessimiste), les requêtes de recherche ne nécessitent pas d'être maintenues dans une transaction d'écriture. Il est recommandé de récupérer les données avant d'ouvrir le bloc transactionnel.

public void updateClientProfile(Long clientId) {
    // Lecture hors transaction
    ClientEntity client = clientRepo.findById(clientId);
    
    // Seule la mise à jour est transactionnelle
    transactionTemplate.execute(status -> {
        clientRepo.updateStatus(client.getId(), "ACTIVE");
        return null;
    });
}

Attention : Si vous scindez une méthode @Transactional en appelant une sous-méthode annotée au sein de la même classe, le proxy Spring AOP ne pourra pas intercepter l'appel, rendant la transaction inopérante. Pour résoudre cela, déléguez l'opération à un service distinct ou utilisez l'auto-injection via AopContext.currentProxy().

3. Éviter les appels distants dans les transactions

L'instabilité du réseau rend les appels RPC ou API imprévisibles. Maintenir une connexion SQL ouverte en attendant la réponse d'un service externe est une cause majeure d'épuisement du pool de connexions.

public void handlePayment(Payment pay) {
    // L'appel externe doit être fait AVANT ou APRÈS la transaction SQL
    boolean success = paymentGateway.authorize(pay.getAmount());
    
    if (success) {
        txTemplate.execute(status -> {
            walletMapper.deduct(pay.getUserId(), pay.getAmount());
            return true;
        });
    }
}

4. Fractionner le traitement de gros volumes

Mettre à jour des milliers de lignes simultanément verrouille une part importante des index de la base. Il est préférable de procéder par lots (batching).

public void batchUpdateStatus(List<Long> ids) {
    int batchSize = 100;
    for (int i = 0; i < ids.size(); i += batchSize) {
        List<Long> subList = ids.subList(i, Math.min(i + batchSize, ids.size()));
        txTemplate.execute(status -> {
            repository.updateBatch(subList);
            return null;
        });
    }
}

5. Isoler les opérations non critiques

Toutes les écritures ne requièrent pas la même atomicité. L'enregistrement de logs d'audit ou la mise à jour de compteurs statistiques peut souvent être réalisé en dehors de la transaction principale pour réduire la durée de détention des verrous.

public void completeTask(Task task) {
    txTemplate.execute(status -> {
        taskRepo.saveStatus(task.getId(), "COMPLETED");
        return null;
    });
    
    // Ce log peut échouer sans invalider la complétion de la tâche
    logService.asyncWriteAudit(task.getId(), "Task finished");
}

6. Adopter le traitement asynchrone

Si certaines actions déclenchées par la trensaction ne nécessitent pas de retour immédiat, l'utilisation de files de messages (MQ) ou de méthodes @Async permet de libérer les ressources transactionnelles plus rapidement.

public void finalizeSale(Sale sale) {
    txTemplate.execute(status -> {
        saleMapper.insert(sale);
        return true;
    });
    
    // Envoyer la notification via un système asynchrone
    messageQueue.send("shipping-exchange", new ShippingEvent(sale.getId()));
}

Étiquettes: Spring Java database optimization TransactionManagement

Publié le 29 juin à 00h20