Criando Sistemas de Reservas consistentes com Pessimistic Locking, Spring Boot e JPA/Hibernate
Tags: java, spring, jpa, concurrency
Published: 2022-08-01 15:31:00

> OBS: Objetivo deste artigo  não é ensinar a construir um sistema de reservas para produção, mas sim introduzir conceitos de controle de concorrência.

Uma cultura bastante popular em diversas cidades no mundo é se encontrar para bater papo e tomar alguns drinks em uma mesa de bar. Em alguns Bares a procura por mesas é tão grande que as pessoas precisam se adiantar para não ficar de fora e isto causa descontentamento aos clientes.
  
Uma possível solução para os donos de bares é prover um mecanismo para que todos os clientes possam reservar suas mesas antecipadamente, porém, dado a alta concorrência pelas mesas, já se pode deduzir que construir um sistema de reserva não é uma tarefa trivial, pois caso o desenvolvedor não se atente aos requisitos não funcionais, mais de um cliente poderá reservar a mesma mesa, ocasionado o que conhecemos como _Anomalia da Atualização Perdida_ ([Lost Update Anomaly](https://vladmihalcea.com/a-beginners-guide-to-database-locking-and-the-lost-update-phenomena/)).

_Anomalia de Atualização Perdida_  causa **_Race Conditions_** ou Condições de Corrida em português. **_Race Conditions_** é um problema classico da Ciência da Computação que indica que se dois ou mais processos, tentam acessar um recurso simultaneamente, após varios acessos não podemos definir o estado do recurso, o que causa um problema de inconsistência.

Para identificar **_Race Conditions_** devemos nos atentar se o algoritmo da sua solução segue o seguinte padrão: **ler - processar- escrever**.  Isto porque durante a janela de tempo entre ler e escrever, outro processo pode atualizar o estado do recurso, causando inconsistência. 

Para entender melhor vamos realizar o seguinte exercício de imaginação. Imagine que dado o modelo de domínio abaixo, dois usuários tentem reservar a mesma mesa paralelamente.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/74d14tzx72pg3d82jdoa.png)

`Usuario 1` realiza a leitura do registro de uma mesa, e realizaria o processamento da lógica de reserva. `Usuario 2` também executa a leitura para o mesmo registro de mesa, e obtêm que a mesa ainda está disponível para reserva. `Usuario 1`   conclui a lógica de reserva, e o `Usuario 2` não é notificado que o status de disponibilidade da mesa mudou, portanto, a  instância em memória da mesa que o  `Usuario 2` possui ainda indica que a mesa está disponível para reserva e dado a isso ele também consegue reservar a mesa.

![fluxo de reserva com race conditions](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y3702mf8asnhtys4xwkw.png)

O diagrama acima pode ser resumido da seguinte forma:

1. `Usuario 1` faz a consulta pela mesa 1, e recebe como resposta que a mesa esta disponivel.
2. `Usuario 2` também consulta a mesa 1, e recebe que a mesa esta disponivel.
3. `Usuario 1` procede com fluxo de reserva, e consegue reservar a mesa.
4. `Usuario 2` contém uma versão em memória que indica que a mesa está disponível, por tanto consegue progredir na reserva, agora tanto `Usuario 1` quanto `Usuario 2` tem uma reserva para mesa 1.

Existem diversas soluções para o problema de **_Race Conditions_**, uma delas é a aplicação do algoritmo de [sessão critica](https://en.wikipedia.org/wiki/Critical_section), que pode ser resumido em, um processo obtem um bloqueio (lock)  modifica o recurso, e libera o bloqueio para que outro processo consiga executar suas alterações.

No mundo Java uma solução popular para **_Race Conditions_** é a fazer com que  a lógica de reserva fique encapsulada em um método com a palavra-chave [`synchronized`](https://www.baeldung.com/java-synchronized). O que proveria a sincronização de acesso ao recurso ao nível de Thread da JVM. O problema desta abordagem é que por requisitos de escalabilidade ou disponibilidade podem existir mais de uma instância da aplicação, indicando a necessidade de um mecanismo de lock distribuído.

Ferramentas como [Zookeeper](https://zookeeper.apache.org/), [MongoDb](https://www.mongodb.com/) e [Redis](https://redis.io/) provem mecanismos para obter bloqueios distribuídos, porém, se em sua infraestrutura já exista um banco de dados relacional, e caso todas as instâncias da sua aplicação acessem este banco é possível se favorecer dos mecanismos de controle de concorrência do banco.

Os Bancos de dados relacionais oferecem suporte a bloqueios exclusivos, adquiridos para evitar conflitos entre operações de escrita. Dado que um bloqueio é adquirido por uma transação, as demais transações que tentem acessar o mesmo recurso serão obrigadas a aguardar que a transação detentora do bloqueio seja confirmada (COMMIT) ou revertida (ROLLBACK).  

Ao inserir um bloqueio exclusivo em uma consulta é utilizado a estratégia de **`Pessimistic Locking`** ou Bloqueio Pessimista em português. 

### Obtendo **`Pessimistic Locking`** em seu banco de dados relacional

Os bloqueios pessimistas podem ser adquiridos de maneira implícita ou explicita. Implícita quando alguma instrução  _SQL_  `UPDATE` ou `DELETE` são disparadas, para que o estado do registro não seja modificado um bloqueio é adquirido. Explicita é quando de maneira declarativa o  usuário do _SGBD_ solicita que a instrução  _SQL_ `SELECT` se comporte como uma operação `UPDATE`, desta forma outras transações são impedidas de escrever, deletar ou adquirir um bloqueio para o registro.

Para solicitar um bloqueio exclusivo em um PostgreSQL basta escrever uma operação _SQL_ `SELECT` com as cláusulas `FOR UDPATE`.

```SQL
  SELECT *
    FROM mesa m
   WHERE m.id = 1
     FOR UPDATE
``` 
> Existem alguns bancos como postgreSQL que oferecem suporte a um bloqueio compartilhado, este bloqueio permite que apenas a transação dentetora do bloqueio solicite um bloqueio exclusivo.
Para solicitar um bloqueio compartilhado em um PostgreSQL basta escrever uma operação _SQL_ `SELECT` com as cláusulas `FOR SHARE`.
>```SQL
>  SELECT *
>    FROM mesa m
>   WHERE m.id = 1
>     FOR SHARE
>```  

### Construindo o mecanismo de bloqueio exclusivo com Spring Data e JPA/Hibernate

O modulo Spring Data JPA oferece suporte a especificação JPA e utiliza Hibernate como implementação padrão, isto significa que nós podemos nos aproveitar dos mecanismos de controle de concorrência da JPA/Hibernate.

Os tipos de bloqueios suportados pela JPA/Hibernate estão descritos no enum `LockModeType`. Abaixo está descrito quais são as propriedades responsáveis por solicitar bloqueio exclusivo ou compartilhado:

- `LockModeType.PESSIMISTIC_READ`: esta propriedade indica que um bloqueio compartilhado deve ser adquirido.
- `LockModeType.PESSIMISTIC_WRITE`: esta propriedade indica que um bloqueio exclusivo deve ser adquirido.

`EntityManager` oferece a possibilidade de realizar um _bloqueio_  através da consulta por id. O método `find()` oferece suporte para através do argumento opcional lockMode que recebe um tipo de lock através do `LockModeType`.

```JAVA
Mesa mesa = entityManager.find(Mesa.class, mesaId, LockModeType.PESSIMISTIC_WRITE);
```

Observe o sql gerado.

```SQL
SELECT m FROM Mesa m WHERE m.id = :id FOR UPDATE
```

Outras formas de solicitar um lock através da `EntityManager` são pelo método `lock()`, e outra alternativa é ao criar uma  `Query` criada através do método `createQuery()` informar a estratégia através do método `setLockMode()`. 

Caso você utilize os repositories (Repository) da Spring Data um lock pode ser solicitado por uma consulta através da anotação **`@Lock(lockMode = LockModeType.PESSIMISTIC_WRITE)`**. No exemplo a baixo é implementado uma consulta por id obtendo um bloqueio exclusivo.

```java
public interface MesaRepository extends JpaRepository<Mesa,Long> {
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT m FROM Mesa m WHERE m.id = :id")
    Optional<Mesa> findByIdWithPessimisticLock(Long id);
}

```

Quando optamos por construir nosso controle de concorrência utilizando um bloqueio exclusivo estamos implementando a estratégia de **`Pessimistic Locking`** ou Bloqueio Pessimista em português.  Isso se deve a estratégia de bloqueio que acredita que a melhor forma de lidar com conflitos é garantindo que eles não existam.

### Construindo o Serviço para Reserva

Durante a construção do serviço de reserva é o momento onde iremos implementar uma sessão critica através da estratégia de  `Pessimistic Locking`. 

A primeiro momento é necessário que esteja bem claro que cada transação é um processo independente, e que o problema de **_Race Condition_** acontece entre a leitura do registro e a persistência da operação, ou seja, durante o período que uma transação esta processando suas  operações outra transação pode alterar o estado do registro.
Então o que precisa ser feito é que dado um contexto transacional é necessário que o registro da Mesa a ser reservada obtenha um bloqueio exclusivo.

Dado isso uma possível implementação para esta logica é:

```java
@RestController
public class ReservarMesaController {
    private final MesaRepository mesaRepository;
    private final UsuarioRepository usuarioRepository;
    private final ReservaRepository reservaRepository;

    public ReservarMesaController(MesaRepository mesaRepository, UsuarioRepository usuarioRepository, ReservaRepository reservaRepository) {
        this.mesaRepository = mesaRepository;
        this.usuarioRepository = usuarioRepository;
        this.reservaRepository = reservaRepository;
    }

    @PostMapping("/mesas/{id}/reservas")
    @Transactional
    public ResponseEntity<?> reservar(
            @PathVariable(value = "id") Long mesaId,
            @RequestBody ReservaMesaRequest request,
            UriComponentsBuilder uriBuilder
    ) {
        Mesa mesa = mesaRepository.findByIdWithPessimisticLock(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();

    }
}
```
Os demais detalhes do código acima estão disponíveis no seguinte [repositório](https://github.com/JordiHOFC/reserva-de-mesa-bar).

A estratégia de **_Pessimistic Locking_** também pode trazer algumas desvantagens, caso sua tabela tenha um alto volume de acesso para operações de atualização e/ou remoção, existe a chance de que varias linhas fiquem bloqueadas, e o impacto disto é que diversas transações ficaram aguardando para serem executadas, o que resultaria em gargalos no banco que se propagariam por toda a aplicação.

## Conclusão

Sistemas que envolvem reservas de recursos são fortes candidatos a sofrem com condições de corrida, isto porquê o espaço de tempo entre a leitura de um recurso e a escrita da solução é suficiente para que outro processo altere o estado do recurso causando inconsistência.

Problema de **_Race Conditions_** podem ser solucionados através da sincronização de acesso ao recurso. Para sincronizar o acesso a um determinado recurso podemos implementar uma sessão critica, que defina que apenas um processo detenha permissão suficiente para alterar o estado do recurso por vez.
 
Os bancos de dados relacionais oferecem mecanismos para controle de concorrência, que podem ser utilizados através das APIs da  JPA/Hibernate. Sobre tudo implementar um mecanismo de controle de concorrência utilizando a estratégia de **_Pessimistic Lock_** é uma tarefa que exige atenção e cuidado, pois o uso de bloqueios exclusivos de maneira imatura podem causar uma baixa produtividade de transações o que afetaria a performance da aplicação.


## Referências

1. [Race conditions](https://blog.avenuecode.com/how-to-avoid-race-conditions-in-your-microservice-application)
2. [Pessimistic Locking com Vlad Mihalcea](https://vladmihalcea.com/hibernate-locking-patterns-how-do-pessimistic_read-and-pessimistic_write-work/)
3. [Pessimistic Locking com Baeldung](https://www.baeldung.com/jpa-pessimistic-locking)
3. [Select FOR Update e For Share em PostgreSQL Doc](https://www.postgresql.org/docs/9.0/sql-select.html#SQL-FOR-UPDATE-SHARE)