public class ClientService {
@Transactional
public ClientDTO method1(Long userId) {
//some code
return method2(userId);
}
@Transactional
public ClientDTO method2(Long userId) {
//some code
ClientDTO client = anotherClass.getClient();
return method3(userId, ClientDTO.getId());
}
@Transactional
public ClientDTO method3(Long userId, Long clientId) {
User user = userRepository.findByIdForUpdate(userId).get();
Client client = clientRepository.findByIdForUpdate(clientId).get();
client.setUser(user);
client.setStatus(Constants.CLIENT_STATUS_CALLED));
user.setStatus(Constants.OPERATOR_STATUS_WAITING_FOR_THE_CALLED_CLIENT);
result = mapper.toDTO(client);
//some code
return result;
}
}
Das Problem besteht darin, dass dieser Algorithmus zu kompliziert ist und die Ausführung zu lange dauert. Und der Dienst läuft in einem Cluster: Es gibt mehrere Dienstinstanzen, die diesen Code parallel ausführen. Es begannen Rennen: Ein Client wurde verschiedenen Operatoren zugewiesen oder einem Operator wurden verschiedene Clients zugewiesen.
Es wurde beschlossen, Sperren auf DB-Ebene einzuführen. Es wurde außerdem beschlossen, den Code, der die Status für den Client und den Operator festlegt, in eine separate Transaktionsmethode zu verschieben, damit dieser Vorgang so atomar wie möglich ausgeführt wird.
public interface UserRepository extends GenericRepository {
@Lock(LockModeType.PESSIMISTIC_WRITE)
Optional findByIdForUpdate(Long id);
}
public interface ClientRepository extends GenericRepository {
@Lock(LockModeType.PESSIMISTIC_READ)
Optional findByIdForUpdate(Long id);
}
public class ClientService {
//@Transactional
public ClientDTO method1(Long userId) {
//some code
return method2(userId);
}
//@Transactional
public ClientDTO method2(Long userId) {
//some code
final short tryCount = 3;
for (int i = 0; i < tryCount; i++) {
ClientDTO client = anotherClass.getClient();
try {
result = method3(userId, ClientDTO.getId());
} catch (ClientIsBusyException e) {
log.info("try again");
continue;
}
return result;
}
}
//@Transactional
public ClientDTO method3(Long userId, Long clientId) throws ClientIsBusyException {
result = clientTransactionalService.clientSetStateCalled(userId, clientId);
//some code
return result;
}
}
public class ClientTransactionalService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public ClientDTO clientSetStateCalled(Long userId, Long clientId) throws ClientIsBusyException {
User user = userRepository.findByIdForUpdate(userId).get();
if(Constants.OPERATOR_STATUS_WAITING_FOR_THE_CALLED_CLIENT.equals(user.getStatus().getId())
throw new IllegalStateException();
Client client = clientRepository.findByIdForUpdate(clientId).get();
if (client.getUser() != null && !Objects.equals(client.getUser().getId(), userId))
throw new ClientIsBusyException();
client.setUser(user);
client.setStatus(Constants.CLIENT_STATUS_CALLED));
user.setStatus(Constants.OPERATOR_STATUS_WAITING_FOR_THE_CALLED_CLIENT);
return mapper.toDTO(client);
}
}
Aber das führte zu anderen Problemen. Nun gibt es eine Menge Code, der nicht in der REQUIRES_NEW-Transaktion enthalten ist. Ich weiß nicht, in welcher Transaktion es ausgeführt wird.
In einigen Szenarios, wenn Methode1 zweimal hintereinander aufgerufen wird, wird beim ersten Mal der Datensatz in der Client-Tabelle gesperrt, aber gibt die Sperre am Ende der clientSetStateCalled-Methode nicht frei. Beim zweiten Aufruf bleibt clientRepository.findByIdForUpdate hängen und wartet darauf, dass die Sperre abläuft.
Ich habe zwei Fragen:
- Wie verwaltet Hibernate Transaktionen, wenn @Transactional nicht festgelegt ist?
- Wie kann ich mit der Methode clientSetStateCalled sicherstellen, dass die Sperre für die Tabellen zu 100 % aufgehoben wird?