DEV Community

Jordi Henrique Silva
Jordi Henrique Silva

Posted on

Criando Sistemas de Reservas Com Versionless Optimistic Locking, Spring Boot e JPA/Hibernate

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
}
Enter fullscreen mode Exit fullscreen mode

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.

Conflito em atualizações com atributos não sobrepostos

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  
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

Hibernate gera o seguinte SQL:

   SELECT *
     FROM mesa m
    WHERE m.id = :mesaId

   UPDATE mesa
      SET quantidadeLugares = :qtdLugares
    WHERE id = :medaId 
      AND quantidadeLugares = 2
Enter fullscreen mode Exit fullscreen mode

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.

Atualizações não sobrepostas

O diagrama acima pode ser resumido em:

  1. A primeira transação (Tx1) realiza a consulta de leitura da entidade Mesa com id igual a 1.
  2. A segunda transação (Tx2) simultaneamente também realiza a consulta de leitura da entidade Mesa com id igual a 1
  3. Tx1 altera a quantidade de lugares na mesa.
  4. 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();
}
Enter fullscreen mode Exit fullscreen mode

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)
                );
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

Referências

Top comments (0)