Pesquisar

quinta-feira, 29 de outubro de 2009

JPA - lock otimista e pessimista, concorrência e afins

JPA - Java Persistence API

JPA é especificação para mapeamento ORM e persistência de dados inspiradíssima no famoso framework Hibernate.

A especificação JPA (1.0) já é amplamente usada e aceita na comunidade de desenvolvedores Java. Há vários provedores de implementação para o padrão ( Hibernate , EclipseLink, TopLink ...)

Um assunto pouco explorado, quando se trata de tutoriais de jpa, é sobre locks (otimista e pessimista). E é sobre esse assunto que tento escrever um pouco aqui.

Tratamento de concorrência em bancos de dados

Há basicamente três tipos de tratamento para concorrência em banco de dados:
  1. Otimista - Quando é criado mecanismos para versionar o dado, no momento em que o banco vai efetivar sua operação sua versão é checada para garantir que você está com um dado que não foi alterado.
  2. Pessimista - O banco simplesmente trava o dado e só aquele que tem a trava consegue trabalhar com os dados.
  3. Ostrich - Quando não há tratamento nenhum pra concorrência :) , ou seja, a maioria dos casos atuais.

O exemplo básico

Começaremos com os perigos do modo Ostrich de agir. Para exemplificar criaremos uma simples classe.
@Entity
public class Produto implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String nome;
private double quantidade;
//getters and setters omitidos
}
Agora considere dois usuários A e B trabalhando sobre o Produto com id = 1( Nintendo Wii) ambos usuários têm o mesmo produto na tela de edição, o usuário A modifica quantidade e salva antes do usuário B, já o usuário B modifica o nome e salva depois. Note que há incosistência, o usuário A acredita (porque ele editou e salvou) que alterou a quantidade de nintendos wii, porém o usuario B (ainda com a quantidade antiga) mudou o nome e salvou o objeto. O que acontece? Depende do seu provedor de JPA, há casos em que o update só é feito para a coluna modificada mas há casos em que o objeto todo é modificado, o que causa claramente inconsistência. Escrever, escrever e escrever pode não ajudar então veja o código abaixo exemplificando esse cenário.
            Produto nintendoWiiA = em.find(Produto.class,1L);
Produto nintendoWiiB = em.find(Produto.class,1L);
/*Usuário A*/
em.getTransaction().begin();
nintendoWiiA.setQuantidade(45.0D);
em.getTransaction().commit();

/*Usuário B*/
em.getTransaction().begin();
nintendoWiiB.setNome("Nintendo WII");
em.getTransaction().commit();

System.out.println(em.find(Produto.class,1L));
ps: O código é apenas para demonstrar como isso é feito, não é funcional.

Agora para mudar esse exemplo para o modo otimista de ser basta criarmos uma propriedade no objeto com a anotação @Version para denotar que este campo será usado para o fim da implementação otimista. Esse campo deve ser int, long ou timestamp.

Com a adição do lock otimista quando o usuário B fosse tentar salvar suas modificações ele receberia a execeção OptimisticLockException. Isso já garantiria ao menos a notificação ao usuário que alguém estava trabalhando sobre o mesmo dado e ( lembra das canseiras que o IDE nos livra no versionamento de código) apartir dai você poderia mostrar o que foi mudado para o usuário tomar a decisão de salvar ou não suas alterações.

A parte chocante fica por conta do modo pessimista de ser, a JPA 1.0 não suporta diretamente o modo pessimista de ser (O JPA 2.0 prevê esse modo a mais :)

Então o que fazer?

Imaginem duas contas bancárias: (sempre esse exemplo) aMinhaConta (com saldo de R$ 150,00) e a suaConta (com saldo de R$ 25.000,00) e você quer me presentear com um Wii mas não tem como mandá-lo pelo correio, logo acha mais conviniente realizar uma transferência para que eu compre. O processo (bem simplificado) se resume a isso:

#0 quantiaASerDoada = R$ 1390,00;
#1 se suaConta.saldo > quantiaASerDoada então
#2 suaConta.transerePara(minhaConta, quantiaASerDoada);
#3 fim se

Se entre a linha 1 e 2 você fizer um saque de R$ 25.000,00 (isso pode ocorrer) a transferência não deveria ser realizada. Um modo para que isso ocorra é travar (lock) o dado que pode sofrer com essas concorrências. No JPA 1.0 você pode travar um objeto em dois passos (ai reside el peligro).

#0 Conta suaConta = em.find(Conta.class, 175789);
#1 em.lock(suaConta,LockMode.Write);

Mais uma vez entre a linha 0 e a 1 pode ocorrer um problema de concorrência. (tanto que a especificação JPA 2.0 já prêve um modo similar ao session do hibernate, o travamento no momento da leitura, em.find(Conta.class, 175789, LockMode.PESSIMISTIC);)

Solução fácil para o problema acima

Basta (se estiver usando Hibernate as your JPA provider) :

((Session) ((EntityManagerImpl) em.getDelegate()).getSession())

O session já tem um jeito de select for update session.get(SuaClasse.class, seuId, tipoDeTravamento). ( já perceberam que pra cada problema que você encontra na JPA 1.0 o hibernate quase sempre tem a solução?!)

Referências

http://en.wikibooks.org/wiki/Java_Persistence/Locking
http://www.javaworld.com/jw-07-2001/jw-0713-optimism.html

2 comentários:

None disse...

Bom post!
Modo Ostrich foi massa! uauahuahuahua

Albino disse...

Valeu bróder! Me ajudou bastante. :-)