2010-02-02 7 views
4

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 fun, nous avons essayé de tester l'opération "insert node" avec JMeter et trouvé un problème de concurrence. Hibernate trouver deux entités rapports avec la même clé privée, juste après un avertissement comme celui-ci:Les transactions spring peuvent-elles désynchroniser une méthode synchronisée?

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

Il est assez facile de voir comment ces problèmes peuvent se produire si plusieurs threads appellent la méthode insert() en même temps.

Mon servlet A appelle un objet de couche de service B.execute(), qui appelle ensuite un objet de couche inférieure C.insert(). (Le code réel est trop grand pour poster, si cela est quelque peu abrégée.)

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 B.execute() synchronized. C'était déjà @Transactional, mais c'est en fait la logique métier qui doit être synchronisée, pas seulement la persistance, ce qui semble raisonnable.

Ma méthode C.insert() était également @Transactional. Mais comme la propagation de transaction par défaut dans Spring semble être requise, je ne pense pas qu'il y ait eu de nouvelle transaction en cours de création pour C.insert().

Tous les composants A, B et C sont des haricots à ressort, et donc des singletons. S'il n'y a vraiment qu'un seul objet B, je conclus qu'il ne devrait pas être possible à 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 charge, des threads supplémentaires s'impliquent, et je vois plusieurs threads imprimer "starting" avant que le premier imprime "finish". Cela semble être une violation de la nature synchronized de la méthode.

J'ai décidé d'imprimer le this dans les messages du journal pour confirmer s'il n'y avait qu'un seul objet B. Tous les messages de journal affichent le même identifiant d'objet. Après beaucoup d'enquête frustrante, j'ai découvert que la suppression du @Transactional pour B.execute() résout le problème. Avec cette ligne disparue, je peux avoir beaucoup de threads, mais je vois toujours un "départ" suivi d'un "finish" avant le prochain "starting" (et mes structures de données restent intactes). D'une certaine manière, le synchronized ne semble fonctionner que lorsque le @Transactional n'est pas présent. Mais je ne comprends pas pourquoi. Quelqu'un peut-il aider? Des conseils sur la façon de regarder plus loin?

Dans les traces de 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 la construction du proxy pourrait ruiner le comportement synchronized.

Répondre

2

Mot-clé synchronisé nécessite, comme vous l'avez indiqué, que l'objet impliqué est toujours le même. Je n'ai pas observé le comportement mentionné ci-dessus moi-même mais votre suspect pourrait juste être correct.

Avez-vous essayé de vous déconnecter de doPost -method?Si c'est différent à chaque fois, il y a de la magie printanière avec des proxies AOP/cglib en cours. Quoi qu'il en soit, je ne m'appuierais pas sur le mot clé syncronized 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 de plusieurs proxies cglib possibles.

+0

Merci Plouh. Je n'étais pas au courant de ReentrantLock - je vais jeter un coup d'oeil. – John

+0

ReentrantLock peut-il résoudre ce problème? – Matt

0

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); 
    ... 
} 
+0

Merci Springfan. Avec votre option B, ne pourrait-il pas y avoir plusieurs instances de ProxyServiceB? Si chacun ne fait que se synchroniser sur lui-même, je ne vois pas en quoi cela apporte quelque bénéfice que ce soit. L'option A (déplacer la couche synchronisée d'une couche) pourrait bien fonctionner, mais il semble dommage de devoir synchroniser toutes les différentes servlets qui pourraient appeler ce service. – John

0

Option 2 Encore une fois:

Effacer la synchronisées 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); 
    ... 
    }  
} 
4

Le problème est que @ Transactionnel encapsule le syn méthode chronisée. Le printemps le fait en utilisant AOP. L'exécution va quelque chose comme ceci:

  1. transaction de départ
  2. appeler la méthode annotée avec @Transactional
  3. lors du retour de la méthode commit la transaction

étapes 1. et 3. peuvent être exécutées par plusieurs threads en même temps. Par conséquent, vous obtenez plusieurs démarrages de transaction.

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

+0

Je crois que c'est la réponse correcte et la plus utile. Les subtilités du printemps sont souvent ignorées et cela conduit à toutes sortes de problèmes inattendus comme celui-ci! –

+0

bonne réponse - nous avons effectué des tests unitaires simultanés pour confirmer cette – mithrandir