jpa - ipp.pt · ao adicionar o atributo version a uma entidade, jpa usa, por omissão, a...
TRANSCRIPT
JPA Demonstração das Estratégias Optimistic Locking e Pessimistic Locking
Locking é uma técnica para tratamento de concorrência em transações em bases de
dados.
Quando duas ou mais transações em bases de dados acedem ao mesmo dado, locking
permite assegurar que apenas uma transação de cada vez consegue mudar esse dado.
Em JPA há duas estratégias para tratar o locking:
• optimistic locking
• pessimistic locking.
Como exemplo vamos considerar as atualizações do saldo de uma conta após dois
depósitos realizados aproximadamente ao mesmo tempo.
Quando dois programas podem modificar o mesmo dado, se ambos os programas
realizam a sequência ler-modificar-escrever de um modo intercalado podem
corromper esse dado. Só o último depósito afeta o saldo perdendo-se a primeira
modificação.
No exemplo que vamos apresentar, uma conta tem o saldo inicial de 100. O programa1
efetua um depósito de 20 e o programa2 efetua um depósito de 30 na mesma conta,
realizando as sequências ler-modificar-escrever de um modo intercalado.
A primeira modificação do saldo perde-se e só o último depósito afeta o saldo, obtendo-
se o saldo final de 130.
programa1
Inicia transação
Efetua depósito 1
Efetua commit
Lê objeto
programa2
Inicia transação
Efetua depósito 2
Efetua commit
Lê objeto
Setup
1. H2 em modo servidor para permitir múltiplas ligações simultâneas Configurar driver na Unidade de Persistência
Configurar h2 para modo Servidor preencher JDBC URL com JDBC URL: jdbc:h2:tcp://localhost/~/bd/Contas Ficheiro persistence.xml:
<?xml version="1.0" encoding="UTF-8"?> <persistence version="2.1" xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"> <persistence-unit name="JPA2PU" transaction-type="RESOURCE_LOCAL"> <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider> <class>Conta</class> <properties> <property name="javax.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/bd/Contas"/> <property name="javax.persistence.jdbc.user" value=""/> <property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/> <property name="javax.persistence.jdbc.password" value=""/> <property name="javax.persistence.schema-generation.database.action" value="create"/> <property name="eclipselink.logging.level.sql" value="FINE"/> <property name="eclipselink.logging.parameters" value="true"/> </properties> </persistence-unit> </persistence>
2. Criar as classes Conta, Iniciar, Util, UtilizadorA e UtilizadorB
@Entity
public class Conta {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private double saldo;
private Conta() {
}
public Conta(double depositoInicial) {
this.saldo = depositoInicial;
}
public double getSaldo() {
return saldo;
}
public void depositar(double deposito) {
saldo += deposito;
}
}
public class Iniciar {
public static void main(String[] args) {
EntityManagerFactory emf
= Persistence.createEntityManagerFactory("JPA2PU");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
Conta conta = new Conta(100);
em.persist(conta);
em.getTransaction().commit();
em.close();
emf.close();
}
}
public class Util {
public static void parar(String msg) {
try {
System.out.println(msg);
System.in.read();
}
catch (Exception e) {}
}
}
public class UtilizadorA {
public static void main(String[] args) {
EntityManagerFactory emf
= Persistence.createEntityManagerFactory("JPA2PU");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
Conta conta = em.find(Conta.class, 1);
Util.parar("A: Saldo=" + conta.getSaldo() + " Continuar?");
conta.depositar(20);
Util.parar("A: Saldo=" + conta.getSaldo() + " Continuar?");
em.getTransaction().commit();
em.close();
emf.close();
System.out.println("A (Fim) - Saldo: " + conta.getSaldo());
}
}
public class UtilizadorB {
public static void main(String[] args) {
EntityManagerFactory emf
= Persistence.createEntityManagerFactory("JPA2PU");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
Conta conta = em.find(Conta.class, 1);
Util.parar("B: Saldo=" + conta.getSaldo() + " Continuar?");
conta.depositar(30);
Util.parar("B: Saldo=" + conta.getSaldo() + " Continuar?");
em.getTransaction().commit();
em.close();
emf.close();
System.out.println("B (Fim) - Saldo: " + conta.getSaldo());
}
}
3. Iniciar h2 em modo servidor h2: h2\bin\h2.bat
4. Executar o método main da classe Iniciar A base de dados é criada.
5. Inspecionar a base de dados criada usando a página H2 Console
Sem Locking 6. Executar intercaladamente os métodos main da classe UtilizadorA e da classe
UtilizadorB. Aplicação funciona incorretamente: Só o último depósito afeta o saldo perdendo-se a primeira modificação.
Numa conta com o saldo de 100 são efetuados dois depósitos, um de 20 e outro de 30,
resultando num saldo final de apenas 130.
Com Optimistic Locking Para usar a estratégia otimista é necessário adicionar uma propriedade dedicada à
persistência para guardar na base de dados o número da versão da entidade.
@Entity
public class Conta {
@Version
private int versionId = 1;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private double saldo;
private Conta() {
}
public Conta(double depositoInicial) {
this.saldo = depositoInicial;
}
public double getSaldo() {
return saldo;
}
public void depositar(double deposito) {
saldo += deposito;
}
}
Este campo de dados versão:
• Não deve ser modificado pela aplicação
• Pode ser numérico ou timestamp mas um valor numérico é recomendado
• É definido pela anotação @Version
No commit da transação a coluna versão da entidade:
• Na estratégia optimistic é incrementada se o estado da entidade mudou durante a
transação
• Na estratégia optimistic_force_increment é incrementada mesmo se o estado da
entidade não mudou durante a transação
Em qualquer uma das duas estratégias otimistas, se no commit da transação, a entidade
tem na base de dados uma versão diferente da versão que tinha quando foi carregada,
uma exceção OptimisticLockException é lançada, significando que outro programa
entretanto modificou a entidade. Podemos apanhar esta exceção e decidir o que fazer.
O locking otimista deixa transações concorrentes processar simultaneamente,
permitindo ler e atualizar uma entidade, mas deteta colisões, verificando no commit se a
versão foi atualizada na base de dados desde que a entidade foi lido, caso em que lança
uma exceção.
A estratégia otimista deve ser usada quando se assume que a maior parte das transações
concorrentes não entram em conflito. As vantagens desta estratégia são não necessitar
de colocar locks na base de dados o que dá melhor escalabilidade. A desvantagem é que
a aplicação deve refrescar e voltar a tentar atualizações que falhem.
Locking de uma entidade
Existem 5 tipos de locking: • OPTIMISTIC
• OPTIMISTIC_FORCE_INCREMENT
• PESSIMISTIC_READ
• PESSIMISTIC_WRITE
• PESSIMISTIC_FORCE_INCREMENT
7. Classe Conta com atributo version Alteração apenas da classe Conta para inclusão do atributo version.
Sem try … catch Necessário criar novamente a base de dados porque agora a tabela Conta tem mais uma coluna (versionId)
Executar intercaladamente os métodos main da classe UtilizadorA e da classe UtilizadorB Aplicação funciona incorretamente Programa termina com exceção lançada.
Método main do UtilizadorB: UPDATE CONTA SET SALDO = ?, VERSIONID = ? WHERE ((ID = ?) AND
(VERSIONID = ?))
bind => [130.0, 2, 1, 1]
Este método main termina com exceção lançada: The object [jpa2.Conta@d109c4f] cannot be updated because it has changed or been deleted since it was last read. Só o primeiro depósito afeta o saldo perdendo-se a última modificação.
Nesta versão do programa duas transações concorrentes tentam atualizar o saldo de um
objeto conta. No commit da primeira transação JPA incrementa o atributo versionId da
conta na base de dados. Quando a segunda transação efetua o commit, JPA lança uma
exceção OptimisticLockException porque o atributo versionId da entidade conta é
diferente do valor que tinha quando a entidade conta foi lida, e a transação é rolled
back.
A aplicação deve refrescar e voltar a tentar efetuar updates que falham.
Com try … catch public class UtilizadorC {
public static void main(String[] args) {
EntityManagerFactory emf
= Persistence.createEntityManagerFactory("JPA2PU");
EntityManager em = emf.createEntityManager();
boolean depositado = false;
do {
try {
em.getTransaction().begin();
Conta conta = em.find(Conta.class, 1);
Util.parar("C: Saldo=" + conta.getSaldo() + " Continuar?");
conta.depositar(20);
Util.parar("C: Saldo=" + conta.getSaldo() + " Continuar?");
em.getTransaction().commit();
depositado = true;
} catch (final Exception e) {
System.out.println("Saldo mudou desde que foi lido. Tente
outra vez.");
}
} while (!depositado);
em.close();
emf.close();
}
}
public class UtilizadorD {
public static void main(String[] args) {
EntityManagerFactory emf
= Persistence.createEntityManagerFactory("JPA2PU");
EntityManager em = emf.createEntityManager();
boolean depositado = false;
do {
try {
em.getTransaction().begin();
Conta conta = em.find(Conta.class, 1);
Util.parar("C: Saldo=" + conta.getSaldo() + " Continuar?");
conta.depositar(30);
Util.parar("C: Saldo=" + conta.getSaldo() + " Continuar?");
em.getTransaction().commit();
depositado = true;
} catch (final Exception e) {
System.out.println("Saldo mudou desde que foi lido. Tente
outra vez.");
}
} while (!depositado);
em.close();
emf.close();
}
}
Executar intercaladamente os métodos main da classe UtilizadorC e da classe UtilizadorD Aplicação funciona corretamente
Output do método main de UtilizadorD:
SELECT ID, SALDO, VERSIONID FROM CONTA WHERE (ID = ?)
bind => [1]
C: Saldo=100.0 Continuar? C: Saldo=130.0 Continuar?
UPDATE CONTA SET SALDO = ?, VERSIONID = ? WHERE ((ID = ?) AND
(VERSIONID = ?))
bind => [130.0, 2, 1, 1]
org.eclipse.persistence.exceptions.OptimisticLockException Exception Description: The object [jpa2.Conta@3023df74] cannot be updated because it has changed or been deleted since it was last read. Saldo mudou desde que foi lido. Tente outra vez.
SELECT ID, SALDO, VERSIONID FROM CONTA WHERE (ID = ?)
bind => [1]
C: Saldo=120.0 Continuar? C: Saldo=150.0 Continuar?
UPDATE CONTA SET SALDO = ?, VERSIONID = ? WHERE ((ID = ?) AND
(VERSIONID = ?))
bind => [150.0, 3, 1, 2]
A aplicação funciona corretamente. O primeiro depósito é efetuado, o segundo depósito
lança a exceção OptimisticLockException, mas a aplicação volta a tentar e efetua
corretamente o segundo depósito. Ao adicionar o atributo version a uma entidade, JPA usa, por omissão, a estratégia
Optimistic Locking, se nenhuma outra estratégia é configurada.
Com Pessimistic Locking 8. Configurar as aplicaçãoes com a estratégia pessimistic locking
O lock numa entidade é colocado pelo EntityManager (em).
Pode ser especificado com um dos seguintes valores: • LockModeType.OPTIMISTIC
• LockModeType.OPTIMISTIC_FORCE_INCREMENT
• LockModeType.PESSIMISTIC_READ
• LockModeType.PESSIMISTIC_WRITE
• LockModeType.PESSIMISTIC_FORCE_INCREMENT
Pode ser colocado:
• Quando a entidade é carregada da base de dados através do método find Conta conta =
em.find(Conta.class, contaPK, LockModeType.PESSIMISTIC_WRITE);
• Através do método refresh em.refresh(conta, LockModeType.PESSIMISTIC_WRITE);
• Quando carregada através de uma query: Query query = em.createQuery(...);
query.setLockMode(LockModeType.PESSIMISTIC_WRITE);
• Depois de ser carregada da base de dados: em.lock(conta, LockModeType. PESSIMISTIC_WRITE);
O modo lock de uma entidade pode ser obtido: LockModeType modoLock = em.getLockMode(entity);
No locking pessimistic, em vez de esperar até ao commit da transação, na esperança de
que nenhuma outra transação tenha mudado os dados, um lock na base de dados é
obtido imediatamente. Assim a transação nunca falha, contudo também não permite
execução paralela de transações.
O locking pessimistic é efetuado ao nível da base de dados enquanto o locking
optimistic é efetuado ao nível da entidade.
Locks pessimistas são propagados para a base de dados usando queries SQL. Se existe
um lock pessimista, a aplicação espera pela base de dados até o lock ser liberto, não
lançando exceção.
public class UtilizadorA {
public static void main(String[] args) {
EntityManagerFactory emf
= Persistence.createEntityManagerFactory("JPA2PU");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
Conta conta = em.find(Conta.class, 1);
em.lock(conta, LockModeType.PESSIMISTIC_WRITE);
Util.parar("A: Saldo=" + conta.getSaldo() + " Continuar?");
conta.depositar(20);
Util.parar("A: Saldo=" + conta.getSaldo() + " Continuar?");
em.getTransaction().commit();
em.close();
emf.close();
}
}
public class UtilizadorB {
public static void main(String[] args) {
EntityManagerFactory emf
= Persistence.createEntityManagerFactory("JPA2PU");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
Conta conta = em.find(Conta.class, 1);
em.lock(conta, LockModeType.PESSIMISTIC_WRITE);
Util.parar("B: Saldo=" + conta.getSaldo() + " Continuar?");
conta.depositar(30);
Util.parar("B: Saldo=" + conta.getSaldo() + " Continuar?");
em.getTransaction().commit();
em.close();
emf.close();
}
}
Executar intercaladamente os métodos main da classe UtilizadorA e da classe UtilizadorB UtilizadorA:
SELECT ID, SALDO, VERSIONID FROM CONTA WHERE (ID = ?) FOR UPDATE
bind => [1]
UtilizadorB:
SELECT ID, SALDO, VERSIONID FROM CONTA WHERE (ID = ?) FOR UPDATE
bind => [1] org.h2.jdbc.JdbcSQLException: Timeout trying to lock table.
Caused by: org.h2.jdbc.JdbcSQLException: Concurrent update in table "CONTA":
another transaction has updated or deleted the same row [90131-194]
Caused by: java.lang.IllegalStateException: Entry is locked [1.4.194/101]
O método main da classe UtilizadorB termina com exceção lançada devido a Timeout.
A ligação (EntityManager) deste programa não consegue obter um lock na base de
dados porque outra ligação (EntityManager de UtilizadorA) mantém o lock. Para obter
o lock seria necessário que a ligação do utilizadorA libertasse o lock durante o “lock
timeout” cujo valor, por omissão, é 1000 ms.
O “lock timeout” pode ser colocado individualmente para cada ligação ou para toda a
aplicação na “Persistence Unity”:
<property name="javax.persistence.jdbc.url"
value="jdbc:h2:tcp://localhost/~/bd/Contas;LOCK_TIMEOUT=80000"/>
Ficheiro persistence.xml:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1" . . . >
<persistence-unit name="JPA2PU" transaction-type="RESOURCE_LOCAL">
<provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
<class>jpa2.Conta</class>
<properties>
<property name="javax.persistence.jdbc.url"
value="jdbc:h2:tcp://localhost/~/bd/Contas;LOCK_TIMEOUT=80000"/>
<property name="javax.persistence.jdbc.user" value=""/>
<property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/>
. . .
</properties>
</persistence-unit>
</persistence>
Agora as aplicações funcionam corretamente Começamos por executar a classe Iniciar para colocar o objeto conta com a versão 1
Em seguida executamos intercaladamente os métodos main da classe UtilizadorA e da classe UtilizadorB, mas a classe UtilizadorB não consegue avançar porque não consegue obter um lock na base de dados UtilizadorA:
SELECT ID, SALDO, VERSIONID FROM CONTA WHERE (ID = ?) FOR UPDATE bind => [1] UPDATE CONTA SET SALDO = ?, VERSIONID = ? WHERE ((ID = ?) AND (VERSIONID = ?)) bind => [120.0, 2, 1, 1]
Após a ligação do UtilizadorA libertar o lock na base de dados, o programa do UtilizadorB avança. UtilizadorB:
SELECT ID, SALDO, VERSIONID FROM CONTA WHERE (ID = ?) FOR UPDATE bind => [1] UPDATE CONTA SET SALDO = ?, VERSIONID = ? WHERE ((ID = ?) AND (VERSIONID = ?)) bind => [150.0, 3, 1, 2]
A principal vantagem de usar pessimistic locking é a garantia de que, uma vez o lock
obtido, a edição terá sucesso. Útil em aplicações altamente concorrentes nas quais
optimistic locking poderia causar muitos erros de locking optimistic.
Uma desvantagem de usar pessimistic locking é que necessita de recursos adicionais da
base de dados, obrigando a transação e a ligação à base de dados serem mantidas
durante a edição.
Locking na base de dados H2 H2 permite múltiplas ligações concorrentes à mesma base de dados. Para garantir que todas
as ligações vejam sempre os dados consistentes, H2 usa, por omissão, locking ao nível da
tabela.
H2 usa 2 tipos de locks:
• read locks ou shared locks
• write locks ou exclusive locks
Geração de locks:
• Instruções SELECT geram locks read
• Instruções que modificam os dados geram locks write
• A instrução SELECT … FOR UPDATE coloca um lock write na tabela sem modificar os
dados
• As instruções COMMIT e ROLLBACK libertam todos os locks
Antes da leitura de um ou mais registos de uma tabela, a base de dados tenta adicionar um shared lock à tabela, o que só é possível se a tabela não tem um exclusive lock. Se o shared lock é adicionado com sucesso, a tabela pode ser lida. Outras ligações podem também obter um shared lock na mesma tabela. Se há um lock exclusive (write) na tabela, então a ligação que quer ler da tabela espera até que a outra ligação liberte o lock exclusive.
Antes da escrita numa tabela (atualizar ou apagar uma linha) é necessário obter um exclusive lock. Para obter um exclusive lock, nenhuma outra ligação pode ter qualquer lock na tabela.
Se uma ligação não consegue obter o lock durante um tempo específico, então uma
exceção timeout é lançada. O timeout por omissão é 1 segundo (1000 ms).
Por omissão H2 usa o nível de isolamento transacional “read committed”:
• os locks read são libertos imediatamente após cada instrução
• os locks write são libertos quando a transação efetua o commit ou o rollback
Resumo:
Na estratégia otimista, que obriga a entidade a ter versão, JPA compara o número da
versão desse objeto na base de dados com o número da versão do objeto em memória.
Se os números das versões são diferentes, o que indica que o objeto já foi modificado
por outro EntityManger, a transação falha e uma exceção OptimisticLockException é
lançada.
Para evitar corrupção dos dados é necessário verificar se há colisões entre programas,
isto é, se há sequências ler, modificar, escrever intercaladas.
Versão na entidade Lock Colisão entre programas
Sem versão
Sem lock, lock NONE Não há verificação
Lock OPTIMISTIC,
OPTIMISTIC_FORCE_INCREMENT,
PESSIMISTIC_FORCE_INCREMENT
Exceção
PersistenceException
Lock PESSIMISTIC_READ,
PESSIMISTIC_WRITE Nunca há colisão
Com versão
Sem lock, lock NONE,
OPTIMISTIC, OPTIMISTIC_FORCE_INCREMENT
Exceção
OptimisticLockException
Lock PESSIMISTIC_READ, PESSIMISTIC_WRITE,
PESSIMISTIC_FORCE_INCREMENT
Nunca há colisão
Mensagens das exceções:
• PersistenceException: Invalid lock mode type on for an entity that does not have
a version locking index. Only a PESSIMISTIC lock mode type can be used
when there is no version locking index.
• OptimisticLockException
Exception Description: The object [model.Conta@229c0fa6] cannot be updated
because it has changed or been deleted since it was last read.
Resumo dos passos na execução da
Demonstração das Estratégias Optimistic Locking e Pessimistic Locking
Numa conta com o saldo de 100 são efetuados dois depósitos. Um programa efetua um
depósito de 20 e outro programa efetua um depósito de 30. As sequências ler-modificar-
escrever são realizadas intercaladamente.
1 – Classe Conta sem atributo Version. Programas sem qualquer estratégia de locking.
Aplicação funciona incorretamente. A primeira modificação do saldo perde-se e só o
último depósito afeta o saldo, obtendo-se o saldo final de 130.
2 – Classe Conta com atributo Version. Programas sem qualquer estratégia de locking.
JPA usa, por omissão, a estratégia Optimistic Locking.
Aplicação funciona incorretamente. A primeira modificação do saldo é efetuada, mas a
execução do segundo depósito lança a exceção OptimisticLockException.
3 – Classe Conta com atributo Version. Programas sem qualquer estratégia de locking
mas com try … catch.
Aplicação funciona corretamente. O primeiro depósito é efetuado, o segundo depósito
lança a exceção OptimisticLockException, mas a aplicação volta a tentar e efetua
corretamente o segundo depósito.
4 – Classe Conta com ou sem atributo Version. Programas com a estratégia Pessimistic
Locking e sem try … catch.
Aplicação funciona incorretamente. O primeiro depósito é efetuado porque obtém um
lock exclusivo na tabela da base de dados. O segundo depósito tenta obter um lock
exclusivo, pelo que espera até o lock da tabela ser liberto. Como o lock timeout, por
omissão, é 1 segundo, ao fim deste tempo a execução do segundo depósito lança a
exceção JdbcSQLException.
5 – Classe Conta com ou sem atributo Version. Programas com a estratégia Pessimistic
Locking e sem try … catch. Lock Timeout colocado com o valor de 80 segundos na
“Persistence Unity”.
Aplicação funciona corretamente. O primeiro depósito é efetuado porque obtém um
lock exclusivo na tabela da base de dados. O segundo depósito tenta obter um lock
exclusivo, pelo que espera até o lock da tabela ser liberto. Obtém o lock exclusivo e
efetua o depósito.