1
votes

Comment faire plusieurs appels de la méthode @Transactional à une seule transaction

J'ai une méthode

@RequestMapping(method = RequestMethod.POST)
public ret_type updateUser(param) {
  // call updateSharedStateByCommunity
}

Cette méthode est appelée à partir de l'API REST suivante:

@Transactional
public void updateSharedStateByCommunity(List[]idList)

Les listes d'ID sont désormais très volumineuses , comme 200000, lorsque j'essaie de le traiter, cela prend beaucoup de temps et une erreur de délai d'attente du côté client s'est produite.

Donc, je veux le diviser en deux appels avec une taille de liste de 100000 chacun.

Mais, le problème est qu'il est considéré comme 2 transactions indépendantes.

NB: Les 2 appels sont un exemple, ils peuvent être divisés en plusieurs fois, si les identifiants numériques sont plus grands .

Je dois assurer deux appels séparés pour une seule transaction. Si l'un des 2 appels échoue, alors il devrait revenir à toutes les opérations.

De plus, du côté client, nous devons afficher la boîte de dialogue de progression, donc je ne peux pas utiliser uniquement le délai d'expiration. P >


10 commentaires

Par défaut, les méthodes @Transactional héritent de la transaction en cours, il vous suffit donc d'appeler cette méthode à partir d'une autre méthode marquée @Transactional . Remarque : vous devrez manuellement vider et effacer la session pour libérer la mémoire RAM pour les objets déjà traités


Annotez la méthode d'appel avec @Transactional et modifiez le Propagation niveau de Transactionnel sur votre méthode - si nécessaire. Il vaut par défaut OBLIGATOIRE - Prend en charge une transaction en cours, créez-en une nouvelle s'il n'y en a pas. Vous n'aurez donc pas à le changer, je crois.


Je pense que le niveau de propagation n'aidera pas. Et le fractionnement se fera côté client. Il n'y a donc aucune transection antérieure à ce moment-là.


Le client attend-il une réponse de sa part?


Oui. Le client attend une réponse.


Comme le client attend une réponse, envisagez d'ajuster le délai d'expiration du client.


Considérant que le temps mort ne fonctionnera pas. Cela prend beaucoup de temps. Nous devons également afficher la boîte de dialogue de progression dans le client.


Comment allez-vous afficher la boîte de dialogue de progression lorsque vous enverrez des listes d'identifiants 100000 à la fois? Comment saurez-vous combien d'entre eux ont été traités?


Lorsque 100000 article terminé, il affichera 50%.


Améliorez simplement votre implémentation dans la updateSharedStateByCommunity en traitant de petits lots et en vidant et en effaçant. Cependant, comme vous n'avez pas inclus la mise en œuvre réelle, nous ne pouvons pas vous aider.


4 Réponses :


0
votes

L'annotation @Transactional accepte un timeout (bien que toutes les implémentations sous-jacentes ne le prennent pas en charge). Je m'opposerais à l'idée de diviser les identifiants en deux appels, et à la place d'essayer de corriger le délai d'expiration (après tout, ce que vous voulez vraiment, c'est une transaction unique, tout ou rien). Vous pouvez définir des délais d'expiration pour toute l'application au lieu d'une méthode par méthode.


4 commentaires

Si l'opération prend 30 minutes, le délai d'attente n'est pas meilleur


@ I.Ahmed si l'opération dure 30 minutes alors il ne sert à rien de faire attendre le client. Faites-en un appel @Async et informez le client dès que le traitement est terminé avec une notification push, etc.


Mon cas d'utilisation ne permet pas d'utiliser Async. Je dois bloquer toute autre opération de l'utilisateur jusqu'à ce qu'elle se termine.


Le délai d'expiration sert uniquement à éviter que la transaction ne soit annulée en raison d'un délai d'expiration. Pour l'expérience utilisateur et les commentaires, procédez comme dit Luis et utilisez Async + WebSockets. Bloquer pendant plus de 30 minutes n'est tout simplement pas une bonne idée.



1
votes

Mon approche préférée dans ces scénarios est de rendre l'appel asynchrone (Spring Boot autorise cela en utilisant l'annotation @Async ), donc le client ne s'attend à aucune réponse HTTP. La notification pourrait être effectuée via un WebSocket qui enverra un message au client avec la progression de chaque X éléments traités.

Cela ajoutera sûrement plus de complexité à votre application, mais si vous concevez correctement le mécanisme, vous pourrez le réutiliser pour toute autre opération similaire à laquelle vous pourriez être confronté à l'avenir.


0 commentaires

0
votes

D'un point de vue technique, cela peut être fait avec la org.springframework.transaction.annotation.Propagation # NESTED Propagation, le comportement NESTED fait que les transactions Spring imbriquées utilisent la même transaction physique mais définit des points de sauvegarde entre les invocations imbriquées afin que les transactions internes puissent également être annulées indépendamment des transactions externes, ou les laisser se propager. Mais la limitation ne fonctionne qu'avec la source de données org.springframework.jdbc.datasource.DataSourceTransactionManager .

Mais pour un jeu de données très volumineux, il faut encore plus de temps pour le traitement et faire attendre le client, donc du point de vue de la solution, peut-être que l'utilisation d'une approche asynchrone sera meilleure, mais cela dépend de vos besoins.


0 commentaires

3
votes

La réponse directe la plus évidente à votre question IMO est de modifier légèrement le code:

@RequestMapping(method = RequestMethod.POST)
public ret_type updateUser(param) {
    updateSharedStateByCommunityBlocks(resolveIds);
}

...

And in Service introduce a new method (if you can't change the code of the service provide an intermediate class that you'll call from controller with the following functionality):

@Transactional
public updateSharedStatedByCommunityBlocks(resolveIds) {
    List<String> [] blocks = split(resolveIds, 100000);  // 100000 - bulk size
    for(List<String> block :blocks) {
       updateSharedStateByCommunity(block); 
    }
}

Si cette méthode est dans le même service, le @Transactional dans l'original updateSharedStateByCommunity ne fera rien donc cela fonctionnera. Si vous mettez ce code dans une autre classe, cela fonctionnera puisque le niveau de propagation par défaut de la transaction de printemps est "Obligatoire"

Donc, il répond à des exigences strictes: vous vouliez avoir une seule transaction - vous je l'ai. Maintenant, tout le code s'exécute dans la même transaction. Chaque méthode fonctionne maintenant avec 100000 et non avec tous les identifiants, tout est synchrone :)

Cependant, cette conception pose problème pour de nombreuses raisons différentes.

  1. Il ne permet pas de suivre la progression (montrez-la à l'utilisateur) comme vous l'avez déclaré vous-même dans la dernière phrase de la question. REST est synchrone.

  2. Cela suppose que le réseau est fiable et attendre 30 minutes n'est techniquement pas un problème (laisser seul l'UX et l'utilisateur «nerveux» qui devra attendre :))

  3. En plus de cela, l'équipement réseau peut forcer la fermeture de la connexion (comme les équilibreurs de charge avec un délai de requête préconfiguré).

C'est pourquoi les gens suggèrent une sorte de flux asynchrone.

Je peux dire que vous pouvez toujours utiliser le flux asynchrone, générer la tâche et après chaque mise à jour groupée, un état partagé (dans -memory dans le cas d'une seule instance) et persistante (comme la base de données dans le cas d'un cluster).

Pour que l'interaction avec le client change:

  1. Le client appelle "updateUser" avec 200 000 identifiants
  2. Le service répond "immédiatement" par quelque chose comme "J'ai reçu votre demande, voici un identifiant de demande, envoyez-moi un ping de temps en temps pour voir ce qui se passe.
  3. Le service démarre une tâche asynchrone et traite les données bloc par bloc en une seule transaction
  4. Le client appelle la méthode "get" avec cet identifiant et le serveur lit la progression à partir de l'état partagé.
  5. Une fois prêtes, les méthodes "Get" répondront "done".

Si quelque chose échoue pendant l'exécution de la transaction, la restauration est effectuée et le processus met à jour l'état de la base de données avec "échec".

Vous pouvez également utiliser des technologies plus modernes pour avertir le serveur (Web sockets par exemple), mais c'est un peu hors de portée pour cette question.

Autre chose à considérer ici: d'après ce que je sais, le traitement de 200000 objets devrait être fait en beaucoup moins de 30 minutes, c'est pas tant que ça pour les SGBDR modernes. Bien sûr, sans connaître votre cas d'utilisation, il est difficile de dire ce qui se passe là-bas, mais vous pouvez peut-être optimiser le flux lui-même (en utilisant des opérations en masse, en réduisant le nombre de requêtes à db, en cache, etc.).

p >


0 commentaires