У меня и моего коллеги есть веб-приложение, которое использует Spring 3.0.0 и JPA (hibernate 3.5.0-Beta2) на Tomcat внутри MyEclipse. Одной из структур данных является дерево. Ради интереса мы попробовали провести стресс-тестирование операции «вставка узла» с JMeter и обнаружили проблему с параллелизмом. Hibernate сообщает об обнаружении двух объектов с одним и тем же закрытым ключом сразу после предупреждения, подобного этому:
WARN [org.hibernate.engine.loading.LoadContexts] fail-safe cleanup (collections) : ...
Довольно легко увидеть, как могут возникнуть такие проблемы, если несколько потоков вызывают метод insert () одновременно.
Мой сервлет A вызывает объект уровня обслуживания B.execute (), который затем вызывает объект уровня C.insert (). (Реальный код слишком велик для публикации, поэтому он несколько сокращен.)
Сервлет A:
public void doPost(Request request, Response response) {
...
b.execute(parameters);
...
}
Служба 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);
}
Субсервис C:
@Transactional
public void insert(params) {
...
// data structure manipulation operations that should not be
// simultaneous with any other manipulation operations called by B.
...
}
Все мои вызовы с изменением состояния проходят через B, поэтому я решил сделать B.execute () synchronized
. Это был уже @Transactional
, но на самом деле нужно синхронизировать бизнес-логику, а не только постоянство, так что это кажется разумным.
Мой метод C.insert () также был @Transactional
. Но так как распространение транзакции по умолчанию в Spring кажется обязательным, я не думаю, что для C.insert () была создана какая-либо новая транзакция.
Все компоненты A, B и C являются пружинными бобами и, следовательно, синглетонами. Если на самом деле существует только один B-объект, то я делаю вывод, что b.execute () не может одновременно выполнять более одной угрозы. Когда нагрузка мала, используется только одна нить, и это так. Но под нагрузкой включаются дополнительные потоки, и я вижу, как несколько потоков печатаются «запускаются», а первые - «заканчиваются». Похоже, это является нарушением synchronized
природы метода.
Я решил напечатать this
в сообщениях журнала, чтобы подтвердить, был ли только один объект B. Все сообщения журнала показывают один и тот же идентификатор объекта.
После долгих разочарований я обнаружил, что удаление @Transactional
для B.execute () решает проблему. С этой строкой у меня может быть много потоков, но я всегда вижу «начало» с последующим «окончанием» перед следующим «началом» (и мои структуры данных остаются неизменными). Каким-то образом synchronized
, кажется, работает только тогда, когда @Transactional
отсутствует. Но я не понимаю почему. Кто-нибудь может помочь? Любые советы о том, как смотреть в это дальше?
В трассировке стека я вижу, что прокси-сервер aop / cglib генерируется между A.doPost () и B.execute (), а также между B.execute () и C.insert (). Интересно, может ли конструкция прокси испортить поведение synchronized
.