Dans les applications Spring modernes, il est courant de combiner l'exécution asynchrone avec un comportement transactionnel. Cependant, annoter une méthode avec @Async et @Transactional(propagation = Propagation.REQUIRES_NEW) peut entraîner un comportement inattendu car Spring gère les tâches et les transactions asynchrones.
Dans cet article, nous explorerons le problème en détail et démontrerons une solution permettant de gérer correctement à la fois l'exécution asynchrone et la gestion des transactions.
Considérez l'extrait de code suivant :
@Async @Transactional(propagation = Propagation.REQUIRES_NEW) public void saveSomething() { // save-point one // save-point two }
À première vue, il peut sembler que tout fonctionne comme prévu. Cependant, cette configuration présente certains problèmes clés qui peuvent entraîner un comportement involontaire.
L'annotation @Async indique à Spring d'exécuter la méthode de manière asynchrone dans un thread séparé. Cela signifie que la méthode ne s'exécutera pas dans le thread d'origine qui l'a appelée mais sera déchargée vers un autre thread dans un pool de threads.
Spring utilise des proxys pour gérer les méthodes asynchrones. Lorsque vous appelez une méthode annotée avec @Async, Spring délègue l'exécution à un exécuteur interne qui exécute la méthode dans un thread différent.
L'annotation @Transactional(propagation = Propagation.REQUIRES_NEW) garantit qu'une nouvelle transaction est démarrée pour la méthode, quelle que soit toute transaction existante. Il suspend toute transaction active dans le thread appelant et commence une nouvelle transaction pour la méthode.
La gestion des transactions dans Spring est généralement liée au thread, ce qui signifie que le contexte de la transaction est lié au thread actuel.
Le problème se pose parce que @Async exécute la méthode dans un thread différent et que la gestion des transactions de Spring s'appuie sur le thread pour lier la transaction. Lorsque la méthode est exécutée de manière asynchrone, le contexte de transaction du thread appelant ne se propage pas au nouveau thread, ce qui entraîne les problèmes suivants :
Pour résoudre ce problème, vous pouvez découpler l'exécution asynchrone de la logique transactionnelle en traitant la transaction dans une méthode de service distincte. Voici comment procéder :
Étape 1 : Créer un nouveau service synchrone pour la logique transactionnelle
Créez un nouveau service qui gère la logique transactionnelle. Cette méthode sera exécutée de manière synchrone (sans @Async) pour garantir que la gestion des transactions fonctionne comme prévu.
Étape 2 : Appeler la méthode synchrone de manière asynchrone
Vous pouvez ensuite appeler la méthode transactionnelle synchrone de manière asynchrone en utilisant @Async. Cela garantit que la logique transactionnelle est gérée correctement dans le thread principal et que le comportement asynchrone est toujours maintenu.
Voici à quoi ressemble le code refactorisé :
@Async @Transactional(propagation = Propagation.REQUIRES_NEW) public void saveSomething() { // save-point one // save-point two }
Dans la solution refactorisée, l'exécution asynchrone est obtenue en annotant la méthode saveSomethingAsync() avec @Async. Cela signifie que lorsque saveSomethingAsync() est appelé, il s'exécutera dans un thread séparé géré par l'exécuteur de tâches asynchrone de Spring. L'exécuter dans un thread différent permet au thread principal de poursuivre son exécution sans attendre la fin de saveSomethingAsync(). Cette approche est bénéfique pour les scénarios dans lesquels vous souhaitez vous décharger de tâches de longue durée, améliorer la réactivité ou gérer simultanément des opérations indépendantes.
Pour le comportement transactionnel, la méthode saveSomething() dans TransactionalService est annotée avec @Transactional(propagation = Propagation.REQUIRES_NEW). Cela garantit que chaque appel à saveSomething() crée une nouvelle transaction indépendante de toute transaction existante dans la méthode appelante. La propagation REQUIRES_NEW démarre une nouvelle transaction et suspend toute transaction existante, permettant à saveSomething() de fonctionner dans un contexte de transaction isolé. Cela signifie que même si la méthode appelante d'origine a une transaction, saveSomething() fonctionnera au sein de sa propre transaction distincte, permettant des validations et des annulations contrôlées pour cette opération uniquement.
En dissociant l'exécution asynchrone de la logique transactionnelle, nous garantissons que la gestion des transactions fonctionne comme prévu. Dans cette configuration, le contexte de transaction reste correctement géré dans la méthode saveSomething(), tandis que la méthode saveSomethingAsync() continue de s'exécuter dans un thread séparé. Cette séparation des préoccupations permet à la fois de bénéficier des avantages du traitement asynchrone et d'une gestion fiable des transactions, permettant des opérations de données indépendantes et sûres même lors d'un traitement simultané.
Lorsque l'isolation des transactions est critique : Si vous devez vous assurer que certaines opérations sont effectuées dans une transaction distincte (c'est-à-dire REQUIRES_NEW), cette approche fonctionne bien.
Opérations asynchrones : Si vous avez des tâches indépendantes de longue durée qui doivent être exécutées de manière asynchrone mais qui nécessitent également leurs propres limites de transaction.
Si vous avez besoin d'un découplage plus avancé ou si vous souhaitez gérer les tentatives, la gestion des erreurs et les processus de longue durée, envisagez de décharger la tâche vers une file d'attente de messages comme Kafka ou RabbitMQ. En utilisant une file d'attente de messages, vous pouvez garantir que chaque tâche s'exécute dans son propre contexte et que la transaction peut être gérée indépendamment.
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!