JPA Demonstração das estratégias optimistic locking e ... · JPA Demonstração das estratégias optimistic locking e pessimistic locking Locking é uma técnica para tratamento
Post on 19-Jul-2020
20 Views
Preview:
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.
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 {
// @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 int getId() {
return id;
}
public double getSaldo() {
return saldo;
}
public void depositar(double deposito) {
saldo += deposito;
}
}
public class Iniciar {
public static void main(String[] args) throws InterruptedException {
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) throws InterruptedException {
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) throws InterruptedException {
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. Executar o método main da classe Iniciar
A base de dados é criada.
4. Iniciar h2 em modo servidor Iniciar o servidor de h2: h2\bin\h2.bat
Inspecionar a base de dados criada usando a página H2 Console
Sem Locking 5. Classe Conta sem atributo version.
Executar intercaladamente os métodos main da classe UtilizadorA e da classe UtilizadorB
@Entity
public class Conta {
// @Version
// private int versionId = 1;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private double saldo;
. . .
}
Aplicação funciona incorretamente: Só o último depósito afeta o saldo perdendo-se a primeira modificação.
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.
Esse 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 ou pelo elemento <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 OptimisticLockingException é lançada, significando que outro thread
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
6. Classe Conta com atributo version Alteração apenas da classe Conta para inclusão do atributo versionId.
Programa termina com exceção lançada 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 Método main do UtilizadorB: UPDATE CONTA SET SALDO = ?, VERSIONID = ? WHERE ((ID = ?) AND
(VERSIONID = ?))
bind => [130.0, 2, 1, 1]
O segundo 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. Com try … catch public class UtilizadorC {
public static void main(String[] args) throws InterruptedException {
EntityManagerFactory emf
= Persistence.createEntityManagerFactory("JPA2PU");
EntityManager em = emf.createEntityManager();
boolean depositado = false;
do {
try {
em.getTransaction().begin();
Conta conta = em.find(Conta.class, 1);
// em.lock(conta, LockModeType.PESSIMISTIC_READ);
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();
}
}
Aplicação funciona corretamente SELECT ID, SALDO, VERSIONID FROM CONTA WHERE (ID = ?)
bind => [1]
UPDATE CONTA SET SALDO = ?, VERSIONID = ? WHERE ((ID = ?) AND
(VERSIONID = ?))
bind => [150.0, 3, 1, 2]
Com Pessimistic Locking 7. Configurar as aplicaçãoes com a estratégia pessimistic locking
O lock numa entidade é colocado pelo EntityManager (em). Pode ser especificado:
Quando a entidade é carregada da base de dados através do método find Pessoa pessoa =
em.find(Pessoa.class, pessoaPK, LockModeType.OPTIMISTIC);
Através do método refresh em.refresh(person, LockModeType.OPTIMISTIC_FORCE_INCREMENT);
Quando carregada através de uma query: Query query = em.createQuery(...);
query.setLockMode(LockModeType.PESSIMISTIC_FORCE_INCREMENT);
Depois de ser carregada da base de dados: em.lock(entity, LockModeType.PESSIMISTIC_READ);
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) throws InterruptedException {
EntityManagerFactory emf
= Persistence.createEntityManagerFactory("JPA2PU");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
Conta conta = em.find(Conta.class, 1);
em.lock(conta, LockModeType.PESSIMISTIC_READ);
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) throws InterruptedException {
EntityManagerFactory emf
= Persistence.createEntityManagerFactory("JPA2PU");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
Conta conta = em.find(Conta.class, 1);
em.lock(conta, LockModeType.PESSIMISTIC_READ);
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.
top related