Implementando optimist locking em aplicações Spring Boot

Marcos
4 min readSep 24, 2020

--

Esses dias no trampo precisei agendar uma tarefa em um programa e me deparei com o seguinte cenário: n instâncias dessa aplicação compartilhando o mesmo banco de dados e executando essa tarefa. Para exemplificar melhor vou criar uma situação aqui:

Tenho uma api que utiliza uma tabela com dados de pessoas, os dados são: nome, idade e cpf. Todo dia as 11h há uma tarefa agendada que conta quantas pessoas cadastradas possuem mais de 30 anos.

Imagine que essa informação vai para um relatório. Agora imagine que em momentos de maior tráfego é necessário subir mais instâncias dessa api para não deixar ninguém na mão, o que pode acontecer ? Repetição dessa informação porque mais de uma instância vai fazer a mesma coisa ..

Nessa, o techlead do time chega e fala algumas opções que podem ser usadas para resolver isso.

Existem algumas formas de fazer isso, dentre elas o optimist locking.

Entendendo um pouco mais:

Para usar o optimist locking é necessário ter uma entidade que contenha uma propriedade com a anotação “ @ Version”. Cada transação que faz leitura dos dados dessa entidade contém o valor do campo anotada com “@ Version”. Antes de fazer uma atualização, a transação verifica essa propriedade novamente e pode ter dois comportamentos: .

  • Se o valor foi alterado durante sua execução, uma exceção do tipo OptimistLockException será lançada.
  • Caso o valor não tenha sido alterado, a transação confirma a atualização e incrementa a propriedade anotada com “@ Version”.

Implementação

Agora que o comportamento está definido (de uma maneira simplória) vou partir para a implementação.

Criei uma classe Pessoa:

@NoArgsConstructor
@Getter
@Setter
@AllArgsConstructor
@Entity
@Table(name = "person")
public class PersonData {

@Id
@GeneratedValue
private int id;

private String name;

private int age;

private String cpf;

@Version
private int controle;

}

Lembra da anotação? Olha ela aí.

Feito isso, a gente pode consultar a tabela criada e verificar esse campo de controle:

Implementei uma query no repository para pegar essa informação:

@Repository
public interface PersonRepository extends JpaRepository<PersonData, Integer> {

@Query("SELECT p FROM Person p where p.age > 29")
List<PersonData> getByAgeAfter30();

}

E criei um caso de uso com um método schedulado para executar a consulta:

@Slf4j
@RequiredArgsConstructor
@Component
public class UseCaseExampleImpl implements UseCaseExample {

private final PersonGateway personGateway;

@Transactional
@Scheduled(cron = "0 1 4 * * *")//sim, escrevi isso as 4h da manhã rs
@Override
public void executar(){
List<PersonData> persons = new ArrayList<>();
persons = personGateway.getByAgeAfter30();
for(PersonData person : persons){
log.info("Name: {}, Age: {}, CPF: {}", person.getName(), person.getAge(), person.getCpf());
}
}

}

Feito isso fiz alguns inserts no banco para ter massa:

insert into person (id, age, controle, cpf, name) values (1, 31, 0,’11111111111', ‘Marcos’); 
insert into person (id, age, controle, cpf, name) values (2, 32, 0,’22222222222', ‘José’);
insert into person (id, age, controle, cpf, name) values (3, 33, 0,’33333333333', ‘Maria’);
insert into person (id, age, controle, cpf, name) values (4, 34, 0,’44444444444', ‘Marta’);
insert into person (id, age, controle, cpf, name) values (5, 35, 0,’55555555555', ‘João’);
insert into person (id, age, controle, cpf, name) values (6, 36, 0,’66666666666', ‘Teste’);
insert into person (id, age, controle, cpf, name) values (7, 22, 0,’33333333333', ‘Maria’);
insert into person (id, age, controle, cpf, name) values (8, 25, 0,’44444444444', ‘Marta’);
insert into person (id, age, controle, cpf, name) values (9, 27, 0,’55555555555', ‘João’);
insert into person (id, age, controle, cpf, name) values (10, 28, 0,’66666666666', ‘Teste’);

Rodei meu programa e até aqui tudo funcionou, o resultado está no log:

Só que rodando duas ou mais instâncias da aplicação as duas conseguirão executar a tarefa e vou ter as informações duplicadas ..

Chegou a hora de mostrar para o framework o que eu quero que ele faça e isso não é difícil, basta incluir mais uma linha na classe repository:

import com.example.estudos.gateway.data.PersonData;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

import javax.persistence.LockModeType;
import java.util.List;

@Repository
public interface PersonRepository extends JpaRepository<PersonData, Integer> {

@Lock(LockModeType.OPTIMISTIC_FORCE_INCREMENT)
@Query(value = "SELECT p FROM PersonData p where age > 30")
List<PersonData> getByAgeAfter30();

}

Essa linha com a anotação “@ Lock” avisa o framework sobre o lock.

Rodando duas instâncias da aplicação o resultado é o seguinte:

Instância 1:

Instância 2:

As linhas que sinalizei mostram que essa instância tentou executar a consulta e antes de terminar a execução do que tinha que fazer aquele atributo anotado com “@ Version” foi atualizado por outra instância, isso significa que outra instância já fez o trabalho e pode-se parar isso aqui.

É legal tratar exceção, mas no geral esse optimist locking está funcionando.

Legal né?

Flw.

--

--

Marcos
Marcos

Written by Marcos

I study software development and I love memes.

No responses yet