Java синхронизированный метод не синхронизирован
У меня есть проект с JAX-RS, Guice, MyBatis. Есть метод getToken()
который вызывается через конечную точку REST. Это синхронизируется, чтобы избежать исключений из-за @Transactional(isolation = Isolation.SERIALIZABLE)
, Однако синхронизированный метод небезопасен, различные вызовы могут одновременно влиять на данные, и возникает исключение:
Cause: org.postgresql.util.PSQLException: ERROR: could not serialize access due to read/write dependencies among transactions
Я попытался синхронизировать по объекту маппера, но он также не работает. Единственное решение, которое работало, было удалить синхронизированный и изменить / удалить уровень изоляции. Как сделать метод синхронизированным?
@Singleton
@Path("/forgottenPath")
public class RestEndpoint {
@Inject
private oneService oneService;
@POST
@Path("/someAction")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public SomeResponse makeSomeAction() {
...
oneService.makeSomeAction();
...
}
}
public class OneServiceImpl implements OneService {
@Inject
private AnotherService anotherService;
@Override
public SomeRespose makeSomeAction() {
...
anotherService.getToken());
....
}
}
@Singleton
public class AnotherServiceImpl implements AnotherService {
@Override
@Transactional(isolation = Isolation.SERIALIZABLE)
public synchronized Token getToken() {
// modifies and retrieves info from database
}
}
1 ответ
Это не о synchronized
не работает должным образом, это о том, как @Transactional
реализовано.
Короче говоря: вместо прямого вызова транзакционного метода (getToken()
в вашем случае) Spring создает прокси-класс, который заменяет все транзакционные методы чем-то вроде этого (очень упрощенно):
// Generated proxy class (either via AOP, dynamic proxy or bytecode generation)
@Override
public Token getToken() {
try {
transactionManager.startTransaction(params);
// Only this call is synchronized
return super.getToken();
}
catch (Throwable e) {
transactionManager.rollback();
rethrow();
}
finally {
// Not in synchronized method (lock is not held), but changes are not commited yet
transactionManager.commit();
transactionManager.closeTransaction();
}
}
Смотрите этот ответ для более подробной информации.
Как видите, сначала открывается транзакция, а затем - ваш оригинал. getToken()
называется, поэтому на самом деле, когда вы пытаетесь получить блокировку (введите синхронизированный метод), транзакция уже создана. Более того, когда звонящий выйдет getToken()
блокировка метода снята (выход из синхронизированного метода), но транзакция еще не зафиксирована. Итак, возможная гонка здесь:
Предположим, что первый поток открывает транзакцию, удерживает блокировку, выполняет работу с базой данных, завершает исходный метод, снимает блокировку и затем делает небольшую паузу. Затем второй поток может выполнять те же действия, первый поток пробуждается, и они оба пытаются зафиксировать, поэтому одна из транзакций должна завершиться неудачей.
Отвечая на исходный вопрос, чтобы избежать изменения уровня изоляции и разрешить сериализованный доступ, вам необходимо синхронизироваться не в вашем сервисе, а вокруг него.
Три решения:
1) Сделать метод вызывающего абонента синхронизированным (makeSomeAction
в твоем случае)
2) Если вы не хотите, чтобы весь метод был синхронизирован, создайте для него блокировку:
@Override
public SomeRespose makeSomeAction() {
...
// Instance of ReentrantLock
lock.lock();
try {
anotherService.getToken());
}
finally {
lock.unlock();
}
....
}
3) Если вы хотите инкапсулировать логику синхронизации, создайте адаптер блокировки:
@Singleton
public class AnotherServiceAdapter implements AnotherService {
@Autowired
private AnotherServiceImpl originalService;
@Override // No transactional here => no aop proxy
public synchronized Token getToken() {
// Lock is held before transactional code kicks in
return originalService.getToken();
}
}