Mitigando problema de concorrência com PostgreSQL e Skip Locked

Evandro Pires
Conta Azul Engineering Blog
4 min readMar 27, 2020

--

Photo by ThisisEngineering RAEng on Unsplash

No sistema contábil da Conta Azul temos um processo periódico, que roda durante o dia todo, para efetuar a depreciação dos ativos imobilizados cadastrados de nossos clientes. Fazemos isso para ter informações em tempo real para os contadores que usam a plataforma.

Ao mesmo tempo, temos a funcionalidade de edição de ativo imobilizado que, a depender das alterações realizadas, precisa recalcular a depreciação desse bem. Um exemplo seria a alteração do tipo do bem que vai impactar no tempo de depreciação do bem e consequentemente os valores que foram depreciados até então.

Se esses processos rodassem ao mesmo tempo, em concorrência, poderíamos ter problemas nos valores a serem contabilizados e precisávamos de uma alternativa para isso.

Existem algumas abordagens possíveis para resolver o mesmo problema, mas a opção que adotamos foi utilizar select for update com skip locked do PostgreSQL.

O que é um select for update?

O select for update é uma forma de realizar um lock pessimista no banco de dados. Basicamente você está dizendo para o banco de dados que você está recuperando um registro que será alterado dentro da mesma transação e, por esse motivo, o banco de dados precisa fazer esperar todos os outros que estão fazendo a mesma coisa para o mesmo registro.

O que é o skip locked?

Skip Locked é uma instrução usada juntamente com o for update para indicar ao banco de dados que você não quer esperar até que o recurso esteja liberado, você quer ignorar que esse registro existe enquanto ele está com lock.

Solução

A ideia para solucionar o problema basicamente é evitar que o job que roda periodicamente pegue um registro que está em edição no momento.

Caso o registro esteja em processo de persistência dos dados por outra funcionalidade, o job não “enxergará” esse mesmo registro e na próxima iteração, com a persistência já realizada, vai voltar a enxergar e realizará a depreciação caso necessário.

Quando o processo de depreciação estiver atuando exatamente no mesmo registro em persistência da edição, esse processo de edição ficará aguardando a finalização do job. Como o job lida com cada registro numa transação separada e o processo é muito rápido, essa espera tende a ser muito pequena.

Para fazer isso tudo funcionar utilizamos os recursos do Hibernate para indicar o LockMode e também que deve ser realizado um skip locked. As alterações necessárias foram poucas, mas foi difícil encontrar essas informações de forma fácil para a implementação.

Como utilizamos Spring Boot nesse projeto, basta fazer algumas anotações para fazer as consultas terem esse comportamento.

A primeira alteração que fizemos foi modificar o método de consulta já existente que busca os registros para o job para realizar um select for update com skip locked. A anotação @Lock(LockModeType.PESSIMISTIC_WRITE) faz com que o Hibernate adicione a instrução for update ao final da consulta montada.
O @QueryHints indicando um timeout com valor de -2 (valor da constante SKIP_LOCKED) faz o Hibernate adicionar a instrução skip locked ao final da consulta.

Uso de select for update com skip locked no método de consulta utilizado pelo job periódico.

Para que o Hibernate consiga colocar a instrução skip locked é necessário estar com o dialeto correto configurado. Utilize o dialeto PostgreSQL95Dialect ou de uma versão mais recente.

Assim é como fica a configuração no application.yaml no Spring Boot:

spring:
jpa:
database-platform: org.hibernate.dialect.PostgreSQL95Dialect

Também alteramos a consulta do registro para edição para fazer select for update para que, caso o registro esteja sendo usado pelo job, ele espere o processo finalizar. Nesse caso, não usamos o skip locked por que a intenção é ficar esperando mesmo.

Exemplo de consulta com select for update apenas para a entidade do from.

Porém, a estratégia nesse método foi diferente, Nesse caso, como a consulta faz um left join fetch para retornar também registros filhos, temos um problema com a instrução padrão que o Hibernate gera. Por padrão, o Hibernate pega todos os aliases gerados e na instrução coloca algo como select ... for update of se, children. Fazendo isso estou gerando lock para ambos registros do SomeEntity e a entidade de children. Como é feito um left join o PostgreSQL reclama dessa consulta pois não consegue fazer lock de um possível retorno null desses casos.
Por esse motivo, tivemos que usar uma abordagem diferente dizendo ao Hibernate que queremos fazer lock em um alias específico.
Para isso temos que usar o query hint org.hibernate.lockMode.alias onde o alias é de fato o alias que você definiu na sua consulta.

Feito isso, conseguimos o objetivo que desejávamos. Quando o job roda e temos uma edição em andamento ele simplesmente ignora o registro. Quando uma edição ocorre e o registro está em processo de depreciação no mesmo momento, ele aguarda até que finalize.

Se você utiliza outros banco de dados também tem possibilidade de fazer algo semelhante. Alguns banco de dados como MySQL e SQL Server tem recursos semelhantes ao skip locked.

--

--

AWS Serverless Hero, CTO at Senior SA, Founder at Sem Servidor podcast, Husband, father of Teodoro and Olivia