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
.
Merci Plouh. Je n'étais pas au courant de ReentrantLock - je vais jeter un coup d'oeil. – John
ReentrantLock peut-il résoudre ce problème? – Matt