5 votes

Les transactions Spring peuvent-elles désynchroniser une méthode synchronisée ?

Mon collègue et moi avons une application web qui utilise Spring 3.0.0 et JPA (hibernate 3.5.0-Beta2) sur Tomcat dans MyEclipse. L'une des structures de données est un arbre. Juste pour le plaisir, nous avons essayé de tester l'opération "insert node" avec JMeter, et nous avons trouvé un problème de concurrence. Hibernate rapporte avoir trouvé deux entités avec la même clé privée, juste après un avertissement comme celui-ci :

 WARN  [org.hibernate.engine.loading.LoadContexts] fail-safe cleanup (collections) : ...

Il est assez facile de voir comment de tels problèmes peuvent se produire si plusieurs threads appellent la méthode insert() simultanément.

Mon servlet A appelle un objet de la couche service B.execute(), qui appelle ensuite un objet de la couche inférieure C.insert(). (Le vrai code est trop gros pour être affiché, donc ceci est quelque peu abrégé).

Servlet A :

  public void doPost(Request request, Response response) {
    ...
    b.execute(parameters);
    ...
  }

Service B :

  @Transactional //** Delete this line to fix the problem.
  public synchronized void execute(parameters) {
    log("b.execute() starting. This="+this);
    ...
    c.insert(params);
    ...
    log("b.execute() finishing. This="+this);
  }

Sous-service C :

  @Transactional
  public void insert(params) {
    ...
    // data structure manipulation operations that should not be 
    // simultaneous with any other manipulation operations called by B.
    ...
  }

Tous mes appels de changement d'état passent par B, donc j'ai décidé de faire en sorte que B.execute() synchronized . C'était déjà @Transactional mais c'est en fait la logique commerciale qui doit être synchronisée, et pas seulement la persistance, donc cela semble raisonnable.

Ma méthode C.insert() était aussi @Transactional . Mais comme la propagation des transactions par défaut dans Spring semble être Requis, je ne pense pas qu'une nouvelle transaction ait été créée pour C.insert().

Tous les composants A, B et C sont des beans à ressort, et donc des singletons. S'il n'existe réellement qu'un seul objet B, j'en conclus qu'il ne devrait pas être possible pour plus d'une menace d'exécuter b.execute() à la fois. Lorsque la charge est légère, un seul thread est utilisé, et c'est le cas. Mais sous la charge, des threads supplémentaires sont impliqués, et je vois plusieurs threads imprimer "starting" avant que le premier n'imprime "finishing". Cela semble être une violation de la règle synchronized la nature de la méthode.

J'ai décidé d'imprimer le this dans les messages du journal pour confirmer qu'il n'y avait qu'un seul objet B. Tous les messages du journal indiquent le même identifiant d'objet.

Après de nombreuses recherches frustrantes, j'ai découvert que le fait d'enlever le @Transactional pour B.execute() résout le problème. Sans cette ligne, je peux avoir beaucoup de threads, mais je vois toujours un "démarrage" suivi d'un "achèvement" avant le prochain "démarrage" (et mes structures de données restent intactes). D'une manière ou d'une autre, le synchronized ne semble fonctionner que lorsque le @Transactional n'est pas présent. Mais je ne comprends pas pourquoi. Quelqu'un peut-il m'aider ? Des conseils sur la façon d'approfondir la question ?

Dans les traces de la pile, je peux voir qu'il y a un proxy aop/cglib généré entre A.doPost() et B.execute() - et aussi entre B.execute() et C.insert(). Je me demande si, d'une manière ou d'une autre, la construction du proxy ne risque pas de gâcher l'exécution du programme. synchronized comportement.

5voto

Dusan.czh Points 1

Le problème est que @Transactional encapsule la méthode synchronisée. Spring fait cela en utilisant la POA. L'exécution se déroule comme suit :

  1. commencer la transaction
  2. appeler la méthode annotée avec @Transactional
  3. quand la méthode revient, valider la transaction

Les étapes 1. et 3. peuvent être exécutées par plusieurs threads en même temps. Vous obtenez ainsi des débuts de transaction multiples.

Votre seule solution est de synchroniser l'appel à la méthode elle-même.

2voto

plouh Points 599

Le mot-clé synchronisé exige, comme vous l'avez dit, que l'objet concerné soit toujours le même. Je n'ai pas observé moi-même le comportement mentionné ci-dessus, mais votre hypothèse pourrait être la bonne.

Avez-vous essayé de vous déconnecter b à partir de doPost -method ? Si c'est différent à chaque fois, alors il y a une certaine magie de Spring avec les proxies AOP/cglib.

Quoi qu'il en soit, je ne compterais pas sur le mot-clé synchronisé mais j'utiliserais quelque chose comme ReentrantLock de java.util.concurrent.locks pour assurer le comportement de synchronisation à la place, car votre objet b est toujours le même, indépendamment des multiples proxies cglib possibles.

0voto

springfan Points 11

Option 1 :

Delete synchronized of ServiceB and:

public void doPost(Request request, Response response) {
    ...
    synchronized(this)
    {
        b.execute(parameters);
    }
    ...
  }

Option 2 :

Delete synchronized of ServiceB and:

public class ProxyServiceB (extends o implements) ServiceB
{
    private ServiceB serviceB;
    public ProxyServiceB(ServiceB serviceB)
    {
        this.serviceB =serviceB;
    }
    public synchronized void execute(parameters) 
    {
         this.serviceB.execute(parameters);
    }
} 

public void doPost(Request request, Response response) 
{
    ...
    ProxyServiceB proxyServiceB = new ProxyServiceB(b);
    proxyServiceB .execute(parameters);
    ...
}

0voto

springfan Points 11

Option 2 à nouveau :

Suppression synchronisée de ServiceB et :

public class ProxyServiceB (extends o implements) ServiceB
{
    private ServiceB serviceB;
    public ProxyServiceB(ServiceB serviceB)
    {
        this.serviceB =serviceB;
    }
    public synchronized void execute(parameters) 
    {
         this.serviceB.execute(parameters);
    }
} 

public class TheServlet extends HttpServlet
{
   private static ProxyServiceB proxyServiceB = null;

   private static ProxyServiceB getProxyServiceBInstance()
   {
        if(proxyServiceB == null)
        {
            return proxyServiceB = new ProxyServiceB(b);
        }
        return proxyServiceB;
   }

   public void doPost(Request request, Response response) 
   {
    ...
     ProxyServiceB proxyServiceB = getProxyServiceBInstance();
    proxyServiceB .execute(parameters);
    ...
   }    
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X