Criando Sistemas de Reservas consistentes com Optimistic Locking, Spring Boot e JPA/Hibernate
Tags: java, spring, jpa, concurrency
Published: 2022-08-11 17:42:00

No [artigo anterior](https://dev.to/jordihofc/criando-sistemas-de-reservas-consistentes-com-pessimistic-locking-spring-boot-e-jpahibernate-1320) foi utilizado a estratégia de **_Pessimistic Locking_** para construção de uma API REST para reserva de mesas de bar. Como visto no artigo existem situações onde empregar bloqueios físicos pode ser custoso visando que se diversas linhas estejam bloqueadas haverá uma diminuição de produtividade nas transações de banco de dados.

Caso você se encontre em uma destas situações, existem outras estratégias para lidar com [Race Conditions](#), como é o caso do  **_Optimistic Locking_** ou Bloqueio Otimista em português.

 **_Optimistic Locking_** é uma estratégia de detecção de conflitos,  ao contrário do `Pessimistic Locking` que seu foco é  evitar conflitos, o bloqueio otimista deixa os conflitos acontecerem e ao identificá-los reverte as transações detentoras dos mesmos. 

## Conhecendo o **_Optimistic Locking_**

A ideia principal do bloqueio otimista é reduzir o período de tempo em que as transações detêm bloqueios físicos afim prover melhorias no desempenho da aplicação. Antes ao utilizar o bloqueio pessimista uma transação obtia um bloqueio garantindo  exclusividade para alterar o recurso. Já no bloqueio otimista diversas transações podem tentar alterar, porém, somente uma ira conseguir e as demais serão abortadas.

Para identificar os conflitos a estrategia utiliza o estado do recurso como um critério de versionamento, permitindo que apenas as transações que contenham o recurso com versão equivalente a presente no banco de dados sejam confirmadas (COMMIT).


### **_Optimistic Locking_** com JPA/Hibernate

JPA/Hibernate oferece suporte para estratégia de **_Optimistic Locking_**, para habilita-lo devemos inserir um campo na **entidade** para representar a versão. Este campo deverá ser anotado com `@Version` para ser gerenciado pela JPA/Hibernate. O atributo de versão deve ser mapeado para `java.sql.Timestamp` ou qualquer tipo que represente números inteiros, por exemplo, `Long, long, int, Integer, short e Short`.

```JAVA
@Entity
public class Mesa {
    @Id
    @GeneratedValue
    private Long id;
    @Version
    private Long version;
    //Demais campos, construtores e metodos omitidos  
}
```

>OBS: É recomendado que o versionamento de entidades seja através de atributos inteiros dado que a versão devera sempre crescer monotonicamente. O perigo de utilizar um atributos de data e hora é que por causa de uma sincronização do  **_Network Time Protocol_** as datas podem retroceder.


Observe o comportamento ao persistir uma entidade.

```Java
@Transactional 
public void cadastrar(){
     Mesa mesa = new Mesa(4);
     entityManager.persist(mesa);
}
```
O SQL gerado pela JPA/Hibernate.

```SQL
INSERT INTO mesa
 (id, quantidadeDeLugares, disponivelParaReserva, version)
VALUES (1, 4, 1, 0);
```

Dado que o mecanismo de bloqueio otimista está habilitado para uma entidade para cada operação de atualização é disparado uma checagem para verificar se a versão da entidade em memória corresponde a versão da entidade no banco de dados, caso corresponda atualização é confirmada e a versão da entidade é incrementada. Caso contrario a transação sera revertida.

Observe abaixo um exemplo de atualização bem sucedida.

```Java
@Transactional 
public void atualizarQuantidadeLugares(Long mesaId, int novaQuantidadeLugares){
     Mesa mesa = entityManager.find(Mesa.class, mesaId);
     mesa.atualizarQuantidadeDeLugares(novaQuantidadeLugares);
}
```


O Hibernate gera a seguinte saída:

```SQL
SELECT * 
  FROM mesa m
 WHERE m.id = :id

UPDATE mesa
   SET quantidadeDeLugares=:qtdLugeres, version = 1
 WHERE id=:id AND version = 0;
```  

Caso o retorno da operação `UPDATE` seja  a contagem de 1 linha atualizada a operação **_SQL_** `UPDATE` sera commitada. Caso seja 0 sera lançada uma Exceção do tipo **`OptimisticLockException`** e automaticamente a transação sera revertida.


Para entender melhor, imagine que duas transações desejem atualizar informações da mesma entidade de maneira simultânea, ambas as transações iram obter a mesma versão de entidade. A primeira transação (Tx1) a terminar, irá persistir suas alterações, e incrementar a versão da entidade. A segunda transação (Tx2) não tem ciência que a versão da entidade foi incrementada, e dispara sua atualização, porém, a versão de presente em memória precede a versão presente no banco, portanto a segunda transação é abortada.


![Evitando uma perda de atualização com Optimistic Locking](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mzlipqpoof8jyu3el18i.png)


## Construindo uma API REST para reserva de mesas com  **_Optimistic Locking_**

Para se beneficiar do mecanismo de **_Optimistic Locking_** em nossa API de reserva, devemos implementar algumas mudanças em comparação a [implementação anterior](https://github.com/JordiHOFC/reserva-de-mesa-bar/blob/master/src/main/java/br/com/github/jordihofc/reserva/mesa/ReservarMesaController.java). Antes onde era adquirido um bloqueio pessimista através da consulta `findByIdWithPessimisticLock( )` agora não é mais necessário dado que uma checagem de versão sera realizada pela JPA/Hibernate. Então podemos substituir a consulta pelo método default `findById( )`.

Um possível implementação para lógica descrita acima é:

```JAVA
@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();
}
```
Sabemos que se uma operação de atualização `UPDATE` falhar no mecanismo de checagem de versão uma `ObjectOptimisticLockingFailureException`será lançada, e se o sistema não estive preparado um erro inesperado sera retornado ao cliente, então é importante que a exceção seja tratada e uma mensagem amigável e coerente ao caso seja dada ao cliente.

Segundo a [RFC 7231](https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.8) quando uma solicitação resulta em erro dado ao estado atual do recurso no servidor de destino é o indicado que a resposta a solicitação contenha o `HTTP Status 409 Conflict`. 


Exemplo de Exception Handler com Spring:

```JAVA
@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 

O mecanismo de bloqueio otimista da JPA/Hibernate trabalha com a estratégia de detecção de conflitos. Isto que significa que o tempo em que o registro fica bloqueado é reduzido. Antes um bloqueio era adquirido no momento da leitura e era liberado ao fim da transação. Agora diversas transações simultâneas podem tentar atualizar o registro, mas apenas uma conseguirá.

Estrategia de bloqueio otimista pode trazer uma melhoria em performance para seu sistema como um todo na maioria dos casos, porém, não se engane, caso o índice de conflitos seja alto o  **_Optimistic Locking_** pode ser mais custoso que um bloqueio pessimista, dado que será necessário um alto esforço do banco de dados para reverter transações. Outro fator que corrobora uma possível perda de performance é que bloqueios otimistas bem implementados requerem retries o que pode aumentar o tempo de resposta da sua aplicação.

Enfim, se seu sistema possui um índice de concorrência baixo ou moderado o uso de **_Optimistic Locking_** é uma ótima opção, pois oferece ganhos de consistência sem sacrificar a performance.

## Referências

- [Optimistic Locking - Vlad Mihalcea](https://vladmihalcea.com/optimistic-locking-version-property-jpa-hibernate/)
- [Optimistic Locking - Baeldung](https://www.baeldung.com/jpa-optimistic-locking)
- [Optimistic Locking](https://eugene-eeo.github.io/blog/optlock.html)
- [Pessimistic vs Optimistic Locking - Vlad Mihalcea](https://vladmihalcea.com/optimistic-vs-pessimistic-locking/)
- [Exception Handler](https://www.baeldung.com/exception-handling-for-rest-with-spring)