Como visto no artigo anterior o Optmistic Locking é uma opção para lidar com Race Conditions para cenários com baixo e/ou moderado índice de conflitos. O principal motivo para o uso de bloqueio otimista nestes cenários é porque não haverá um número exagerado de transações conflitantes e o seu banco de dados (BD) não será sobrecarregado com uma alta quantidade de rollbacks.
Caso seu sistema necessite atualizar atributos distintos de uma entidade simultaneamente o Optmistic Locking poderá se tornar um empecilho dado que o mecanismo de bloqueio otimista encara as atualizações como tudo ou nada, ou seja, caso a versão da entidade em memória não corresponda a presente ao BD a transação será abortada independente se os campos atualizados não sejam sobrepostos.
Para entender melhor, dado a entidade Mesa descrita no código abaixo.
@Entity
public class Mesa {
@Id
@GeneratedValue
private Long id;
@Column(nullable = false)
private Integer quantidadeDeLugares;
@Column(nullable = false)
private boolean disponivelParaReserva;
@Version
private Long version;
//construtores e metodos omitidos
}
Imagine que duas transações simultâneas desejem atualizar os atributos quantidadeDeLugares
e disponivelParaReserva
. Ambas consultem o registro e recebam a mesma versão da entidade Mesa
. A primeira transação (Tx1) atualiza a quantidade de lugares, fazendo com que a versão da mesa seja incrementada. Enquanto a segunda transação (Tx2) tenta atualizar o status da mesa para indisponível para reserva, porém, dado que Tx1 incrementou a versão da entidade, agora a versão está obsoleta, portanto, Tx2 sera abortada.
Em casos de uso onde é necessário que atualizações com atributos não sobrepostos não conflitem, ou quando alterar o schema de dados para inserir um atributo de versão seja caro independente do motivo o Hibernate oferece suporte ao controle de concorrência com mecanismo de Versionless Optmistic Locking ou bloqueio otimista sem versão em português.
Version-less optimistic locking
Os bloqueios otimistas estão relacionados a um atributo incremental para controle de versão da entidade, e é através deste que a integridade e consistência são mantidas em atualizações concorrentes.
Existem sistemas onde o custo de utilizar uma estratégia de Optimistic Locking é alto, pois não é simples inserir uma nova coluna na tabela dado que o impacto desta mudança poderá causar anomalias em outras funcionalidades do sistema, ou para realizar mudanças no schema exija aprovações em um sistema altamente burocrático, ou você trabalha em um sistema legado onde alterar o schema não é uma opção.
Para estes casos é possível criar um versionamento da entidade a partir do próprio estado do recurso, seja através de uma coluna atualizada ou da junção de todas colunas.
Versionless Optmistic Locking com Hibernate
Hibernate oferece suporte ao controle de concorrência com bloqueio otimista sem versão, para habilitar o mesmo devemos utilizar anotação @OptimisticLocking
ao nível de entidade, esta anotação recebe como argumento a estratégia de bloqueio otimista descrita por um OptimisticLockType
.
O Hibernate oferece suporte as seguintes estratégias de bloqueio otimista.
-
OptimisticLockType.NONE
: indica que o mecanismo de bloqueio otimista está desabilitado; -
OptimisticLockType.VERSION
: indica que o mecanismo de bloqueio otimista utilizará o atributo anotado com@Version
como versão da entidade; -
OptimisticLockType.DIRTY
: indica que o mecanismo de bloqueio otimista utilizará apenas os atributos atualizados pela transação como versão da entidade; -
OptimisticLockType.ALL
: indica que o mecanismo de bloqueio otimista utilizar todos os atributos da entidade como versão;
Como o mecanismo de controle de concorrência precisa inserir condições adicionais em sua Query
de atualização, é necessário que a entidade seja anotado com @DynamicUpdate
ao nível de entidade.
@Entity
@OptimisticLocking(type = OptimisticLockType.ALL)
@DynamicUpdate
public class Mesa {
@Id
@GeneratedValue
private Long id;
//Demais campos, construtores e metodos omitidos
}
Controle de Concorrência com OptimisticLockType.ALL
Dado que a estratégia de bloqueio otimista OptimisticLockType.ALL
esteja habilitada no momento de uma atualização o Hibernate irá verificar se o estado de cada atributo da entidade corresponde em memória ao estado presente no BD.
Para melhor entendimento observe o código abaixo.
@Transactional
public void atualizarQuantidadeDeLugares(Long mesaId, int qtdLugares){
Mesa mesa = entityManager.find(Mesa.class, mesaId);
mesa.setQuatidadeDeLugares(qtdLugares);
}
Hibernate gera o seguinte SQL:
SELECT *
FROM mesa m
WHERE m.id = :mesaId
UPDATE mesa
SET quantidadeLugares = :qtdLugares
WHERE id = :medaId
AND quantidadeLugares = 2
AND disponivelParaReserva = true
No SQL acima é possível visualizar que todos os campos se uniram e convergiram em um único atributo de versão global. Esta estratégia é utilizada quando é impossível inserir um atributo para versionamento da entidade. Em contrapartida, as atualizações com atributos não sobrepostos conflitam.
Controle de Concorrência com OptimisticLockType.DIRTY
A estratégia de bloqueio otimista OptimisticLockType.DIRTY
favorece que apenas os campos "sujos" na atualização sejam utilizados como versão da entidade, ou seja, agora atualizações com atributos não sobrepostos não conflitam mais.
Observe o comportamento do seguinte método de atualização.
@Transactional
public void atualizarQuantidadeDeLugares(Long mesaId, int qtdLugares){
Mesa mesa = entityManager.find(Mesa.class, mesaId);
mesa.setQuatidadeDeLugares(qtdLugares);
}
Hibernate gera o seguinte SQL:
SELECT *
FROM mesa m
WHERE m.id = :mesaId
UPDATE mesa
SET quantidadeLugares = :qtdLugares
WHERE id = :medaId
AND quantidadeLugares = 2
O SQL apresentado acima demostra que apenas o atributo quantidadeLugares
foi utilizado na condição do WHERE
, ou seja, quando a estratégia de bloqueio otimista DIRTY
é utilizada apenas os campos atualizados são utilizados como versão. O sistema permite que atualizações a atributos distintos não conflitem evitando OptimisticLockException
.
O diagrama acima pode ser resumido em:
- A primeira transação (Tx1) realiza a consulta de leitura da entidade Mesa com id igual a 1.
- A segunda transação (Tx2) simultaneamente também realiza a consulta de leitura da entidade Mesa com id igual a 1
- Tx1 altera a quantidade de lugares na mesa.
- Tx2 altera o status de disponibilidade da mesa
Construindo uma API REST para reserva de mesas com Versionless Optimistic Locking
Ao nível de controller não existem mudanças a implementação do artigo anterior dado que nossas mudanças afetam apenas as entidades.
@PostMapping("/mesas/{id}/reservas")
@Transactional
public ResponseEntity<?> reservar(
@PathVariable(value = "id") Long mesaId,
@RequestBody ReservaMesaRequest request,
UriComponentsBuilder uriBuilder
) {
Mesa mesa = mesaRepository.findById(mesaId)
.orElseThrow(() -> new ResponseStatusException(NOT_FOUND, "Mesa não cadastrada"));
Usuario usuario = usuarioRepository.findById(request.getUsuarioId())
.orElseThrow(() -> new ResponseStatusException(UNPROCESSABLE_ENTITY, "Usuario não cadastrado"));
LocalDateTime dataDaReserva = request.getDataReserva();
if (reservaRepository.existsMesaByIdAndReservadoParaIs(mesaId, dataDaReserva)) {
throw new ResponseStatusException(UNPROCESSABLE_ENTITY, "Horario indisponivel para reserva");
}
Reserva reserva = mesa.reservar(usuario, dataDaReserva);
reservaRepository.save(reserva);
URI location = uriBuilder.path("/mesas/{id}/reservas/{reservaId}")
.buildAndExpand(mesa.getId(), reserva.getId())
.toUri();
return ResponseEntity.created(location).build();
}
Independente da abordagem de bloqueio otimista sem versão é indispensável a construção de um Exception Handler para ObjectOptimisticLockingFailureException
, quando atualizações conflitarem é essencial que a resposta contenham um Http Status Conflict
.
@RestControllerAdvice
public class DeafultExceptionHandler {
@ExceptionHandler(ObjectOptimisticLockingFailureException.class)
public ResponseEntity<?> optmisticLock(ObjectOptimisticLockingFailureException ex) {
String msg = "Aconteceu um conflito em sua atualização, tente novamente.";
return status(409)
.body(
Map.of("mensagem",msg)
);
}
}
Conclusão
Construir mecanismo para controle de concorrência é uma tarefa complexa dado que cada sistema possui suas restrições. O mecanismo de Versionless Optimistic Locking do Hibernate oferece flexibilidade para implementação da estratégia de detecção de conflitos para diversos casos de uso.
Caso o conflito de atualizações estejam causando integridade aos dados em um sistema legado ou quando alterar o schema seja custoso o OptimisticLockType.ALL
se torna uma ótima solução dado que seu comportamento é semelhante ao Optimistic Locking tradicional.
Porém, se existe a necessidade de que atualizações a atributos não sobrepostos não conflitem a estratégia de OptimisticLockType.DIRTY
se torna uma solução já que apenas os atributos "sujos" são utilizados como versão da entidade.
Top comments (0)