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.