Concorrência com Hibernatepor: Raphaela Galhardo
(raphaela@jeebrasil.com.br) Este tutorial apresenta conceitos relacionados a concorrência entre transações e como elas podem ser tratadas utilizando o framework Hibernate. 1. IntroduçãoEm algumas situações pode acontecer que duas ou mais transações que ocorrem paralelamente leiam e atualizem o mesmo dado. Considerando que duas transações leiam um mesmo dado x quase que simultaneamente. Ambas as transações vão manipular esse mesmo dado com operações diferentes e atualizá-lo na base de dados. Para exemplificar, a Listagem 1 apresenta um exemplo de duas transações concorrentes manipulando o mesmo dado x. No primeiro passo, ambas as transações lêem o dado x com o mesmo valor (2). Em seguida, T1 soma o valor x que leu com 1 e o valor de x para T1 passa a ser 3 (2 + 1). Já T2, soma o valor de x lido a 3 e x passa a ter o valor 5 (2 + 3). Por fim, ambos T1 e T2 gravarão os novos valores de x calculados na base de dados, respectivamente. Como não há controle de concorrência de acesso ao dado x, o seu valor final corresponderá a 5, ou seja, o valor calculado por T2, significando que as alterações feitas por T1 foram descartadas. 1) Transação 1 (T1) lê x = 2 2) Transação 2 (T2) lê x = 2 3) T1 faz x = x + 1 4) T2 faz x = x + 3 5) T1 armazena o valor de x na base de dados 6) T2 armazena o valor de x na base de dados Listagem 1 - Exemplo de Transação Concorrente Para evitar a situação descrita anteriormente, deve-se controlar o acesso concorrente ao dado, ou seja, deve-se implementar o mecanismo de Locking. O gerenciamento de locking e da concorrência pode ser feito de duas formas:
2. Lock OtimistaPara ilustrar o gerenciamento do tipo otimista, um exemplo é dado a partir das Figura 1 e 2. O problema é mostrado na Figura 1, onde, inicialmente, duas transações (ilustradas por Thread 1 e Thread 2) acessam um mesmo dado na base de dados (SELECT Dado), uma seguida da outra. Logo em seguida, a primeira transação (Thread 1) atualiza este dado na base de dados e depois quem também o atualiza é a segunda transação (Thread 2). Nesta abordagem otimista, acontece que a atualização do dado feita pela segunda transação sobrescreve a atualização realizada pela primeira, ou seja, a atualização feita pela primeira transação é perdida.
Figura 1 - Loocking Otimista 1 Para resolver o problema descrito anteriormente com a abordagem otimista, pode-se utilizar o conceito de Version Number, que é um padrão utilizado para versionar numericamente os dados de uma linha de uma tabela na base de dados. Por exemplo, na Figura 2, também, inicialmente, duas transações (ilustradas por App 1 e App 2) acessam um mesmo dado na base de dados (SELECT Dado), uma seguida da outra. Com isso, esse mesmo dado nas duas transações são rotulados com a versão atual dele na base de dados, no caso, Versão 1. Logo em seguida, a segunda transação atualiza este dado. Quando a atualização vai ser feita, é verificado se a versão do dado na transação corresponde à versão dele na base de dados. Nesta primeira atualização, a versão da transação é 1 e a da base de dados também. Como elas são iguais, a atualização é efetivada e a versão do dado na base de dados passa a ser a Versão 2. Por fim, a primeira transação vai também atualizar este mesmo dado. Dessa forma, também é feita uma comparação entre as versões do dado na transação e na base de dados. Neste caso, a versão na transação é a 1 e na base de dados é 2, ou seja, as versões não correspondem. Assim, um erro é disparado e a atualização desejada pela primeira transação não é concretizada, evitando que a atualização feita pela segunda transação não seja desfeita.
Figura 2 - Loocking Otimista 2 Com o Hibernate, uma forma de utilizar o versionamento dos dados é utilizar o elemento version no mapeamento das tabelas. Para exemplificar o seu uso, considera-se a classe ContaCorrente na Listagem 2 que será mapeada para a tabela CONTA_CORRENTE na base de dados. Dentre os diversos atributos da classe, está o atributo denominado versao que irá justamente guardar a versão atual das linhas da tabela. A tabela CONTA_CORRENTE também deve ter uma coluna para onde esse atributo versao será mapeado. package br.com.jeebrasil.dominio.ContaCorrente; public class ContaCorrente{ //Atributo utilizado para o versionamento private int versao; //Demais atributos private int id; private double saldo; private Correntista correntista; //Outros atributos //... public int getVersao(){ return versao; } public setVersao(int versao){ this.versao = versao; } //Demais métodos de acesso e modificação de dados //... } Listagem 2 - Classe de Domínio: ContaCorrente 1 No arquivo de mapeamento da classe ContaCorrente, ContaCorrente.hbm.xml (ver Listagem 3), o atributo version é utilizado para mapear o atributo versao da classe ContaCorrente para a coluna VERSAO da tabela CONTA_CORRENTE. Com esse mapeamento, toda vez que uma determinada linha for atualizada na base de dados, a sua coluna VERSAO será incrementada de uma unidade, indicando a nova versão dos dados. <hibernate-mapping> <class name="br.com.jeebrasil.dominio.ContaCorrente" table="CONTA_CORRENTE"> <id name="id" column="ID_CONTA" type="int"> <generator class="sequence"> <param name="sequence">conta_seq</param> </generator> </id> <version name="versao" column="VERSAO"/> <property name="saldo" type="double" column="SALDO"/> <many-to-one name="correntista" class="br.com.jeebrasil.dominio.Correntista" column="ID_CORRENTISTA"/> <!-- Demais Atributos --> </class> </hibernate-mapping> Listagem 3 - ContaCorrente.hbm.xml 1 Outra forma de implementar o lock otimista é utilizando o atributo timestamp no mapeamento da tabela. Neste caso, a classe ContaCorrente do exemplo anterior ao invés de ter um atributo inteiro para guardar a versão do dado, teria uma atributo do tipo java.util.Date para guardar o instante no tempo da última atualização. Neste caso, a classe de domínio seria equivalente à mostrada na Listagem 4 e seu mapeamento ao exibido na Listagem 5. Neste exemplo, a tabela CONTA_CORRENTE deve conter uma coluna do tipo timestamp denominada DATA_ULTIMA_ATUALIZACAO. package br.com.jeebrasil.dominio.ContaCorrente; public class ContaCorrente{ //Atributo utilizado para o versionamento private Date ultimaAtualizacao; //Demais atributos //... public Date getUltimaAtualizacao(){ return ultimaAtualizacao; } public setUltimaAtualizacao(Date ultimaAtualizacao){ this.ultimaAtualizacao = ultimaAtualizacao; } //Demais métodos de acesso e modificação de dados //... } Listagem 4 - Classe de Domínio: ContaCorrente 2 <hibernate-mapping> <class name="br.com.jeebrasil.dominio.ContaCorrente" table="CONTA_CORRENTE"> <id name="id" column="ID_CONTA" type="int"> <generator class="sequence"> <param name="sequence">conta_seq</param> </generator> </id> <timestamp name="ultimaAtualizacao" column="DATA_ULTIMA_ATUALIZACAO"/> <!-- Demais Atributos --> </class> </hibernate-mapping> Listagem 5 - ContaCorrente.hbm.xml 2 Se a tabela não possuir uma coluna para guardar a versão do dado ou a data da última atualização, com Hibernate, há uma outra forma de implementar o lock otimista, porém essa abordagem só deve ser utilizada para objetos que são modificados e atualizados em uma mesma sessão (Session). Se este não for o caso, deve-se utilizar uma das duas abordagens citadas anteriormente. Com essa última abordagem, quando uma determinada linha vai ser atualizada, o Hibernate verifica se os dados dessa linha correspondem aos mesmos dados que foi recuperado. Caso afirmativo, a atualização é efetuada. Para isso, no mapeamento da tabela, deve-se incluir o atributo optimistic-lock (Listagem 6). Por exemplo, no mapeamento da classe ContaCorrente, na tag class apareceria este atributo. <hibernate-mapping> <class name="br.com.jeebrasil.dominio.ContaCorrente" table="CONTA_CORRENTE" optimist-lock="all"> <id name="id" column="ID_CONTA" type="int"> <generator class="sequence"> <param name="sequence">conta_seq</param> </generator> </id> <property name="saldo" type="double" column="SALDO"/> <property name="descricao" type="java.lang.String" column="DESCRICAO"/> <many-to-one name="correntista" class="br.com.jeebrasil.dominio.Correntista" column="ID_CORRENTISTA"/> <!-- Demais Atributos --> </class> </hibernate-mapping> Listagem 6 - ContaCorrente.hbm.xml 3 Dessa maneira, quando uma linha dessa tabela fosse atualizada, o SQL equivalente gerado para a atualização seria o exibido na Listagem 7. Neste exemplo, considera-se a atualização do saldo para R$ 1.500,00 de uma determinada conta de saldo R$ 1.000,00. UPDATE CONTA_CORRENTE SET SALDO = 1500 WHERE ID_CONTA = 104 AND SALDO = 1000 AND DESCRICAO = "DESCRICAO DA CONTA" AND ID_CORRENTISTA = 23 Listagem 7 - Exemplo optimist-lock="all" 3. Lock PessimistaA estratégia de lock pessimista para proibir o acesso concorrente a um mesmo dado da base de dados é feita bloqueando o mesmo até que a transação seja finalizada. Alguns banco de dados, como o Oracle e PostgreSQL, utilizam a construção SQL SELECT FOR UPDATE para bloquear o dado até que o mesmo seja atualizado. O Hibernate fornece um conjunto de modos de lock (constantes disponíveis na classe LockMode) que podem ser utilizados para implementar o lock pessimista. Considerando o exemplo da Listagem 8, onde um determinado aluno é consultado na base de dados e tem seu nome atualizado. Neste caso, não há um bloqueio ao dado, então qualquer outra transação pode acessar este mesmo dado concorrentemente e modifica-lo, de forma que poderá ocorrer uma incosistência dos dados. Na Listagem 9, há um exemplo de uso do lock pessimista para resolver este problema, bastando passar a constante LockMode.UPGRADE como terceiro argumento do método get do objeto Session. Transaction tx = session.beginTransaction(); Aluno aluno = (Aluno) session.get(Aluno.class, alunoId); aluno.setNome("Novo Nome"); tx.commit(); Listagem 8 - Transação sem Lock Transaction tx = session.beginTransaction(); Aluno aluno = (Aluno) session.get(Aluno.class, alunoId, LockMode.UPGRADE); aluno.setNome("Novo Nome"); tx.commit(); Listagem 9 - Transação com Lock Pessimista: LockMode.UPGRADE O método get do objeto Session pode receber como terceiro argumento para implementar o lock pessimista as seguintes constantes:
4. Referências
|