1. Introdução
Então, vamos começar! O que a
anotação de versão significa na JPA?
Em suma, ela é responsável pelo bloqueio na APP. Esta anotação resolve um dos problemas que podem surgir como resultado de transações paralelas.
Que problemas podem surgir?
- As atualizações perdidas podem ocorrer em situações em que duas transações em paralelo tentam atualizar os mesmos dados.
- Leituras sujas ocorrem quando uma transação vê alterações que ainda não foram salvas por outra transação. Nesses casos, um problema pode ocorrer devido à reversão da segunda transação, mas os dados já foram lidos primeiro.
- As leituras não repetíveis ocorrem quando a primeira transação recebe dados, e a segunda transação faz alterações nelas e as confirma com êxito até o final da primeira transação. Em outras palavras, quando dentro da mesma transação a mesma solicitação para obter, por exemplo, a tabela inteira, retorna resultados diferentes.
- A leitura fantasma é um problema semelhante à repetição de leituras, com a exceção de que um número diferente de linhas é retornado.
Brevemente sobre suas decisões
- LEIA NÃO COMPROMISSO - resolvido usando a anotação Versão na JPA (este é exatamente o artigo)
- READ COMMITED - permite ler apenas alterações confirmadas
- LEITURA REPETÍVEL - um pouco mais complicado aqui. Nossa transação "não vê" alterações nos dados lidos anteriormente e outras transações não podem alterar os dados que caíram em nossa transação.
- SERIALIZABLE - execução seqüencial de transações
Cada parágrafo subsequente abrange todos os anteriores, ou seja, pode substituir as soluções indicadas anteriormente. Portanto, SERIALIZABLE possui o nível mais alto de isolamento e READ UNCOMMITED possui o menor.
Versão
Versão resolve o problema com
atualizações perdidas . Como exatamente, agora vamos ver.
Antes de passar para o código, vale ressaltar que existem dois tipos de bloqueios:
otimista e
pessimista . A diferença é que os primeiros são guiados por situações nas quais muitas transações tentam alterar um campo ao mesmo tempo, surgem extremamente raramente, enquanto outros são guiados pela situação oposta. De acordo com isso, há uma diferença em sua lógica de execução.
Em bloqueios
otimistas , ao confirmar o banco de dados, o valor do campo marcado como versão é comparado no momento em que os dados são recebidos e no momento. Se ele mudou, ou seja, alguma outra transação está à frente da nossa e conseguiu alterar os dados, nesse caso, nossa transação gera um erro e é necessário reiniciá-lo.
Ao usar bloqueios
otimistas , é garantido um nível mais alto de competitividade ao acessar o banco de dados, mas, neste caso, você deve repetir transações que não tiveram tempo de fazer alterações antes dos outros.
Nos
pessimistas , o bloqueio é imposto imediatamente antes da suposta modificação de dados em todas as linhas que essa modificação supostamente afeta.
E ao usar bloqueios
pessimistas , a ausência de contradições durante a execução da transação é garantida, devido a colocar o restante no modo de espera (mas leva tempo), como resultado, diminuindo o nível de concorrência.
LockModeType ou como definir um bloqueio
O bloqueio pode ser definido chamando o método de aparência do EntityManager.
entityManager.lock(myObject, LockModeType.OPTIMISTIC);
LockModeType define uma estratégia de bloqueio.
LockModeType pode ser de 6 tipos (2 dos quais são
otimistas e 3 são
pessimistas ):
- NENHUM - sem bloqueio
- OPTIMISTIC
- OPTIMISTIC_FORCE_INCREMENT
- PESSIMISTIC_READ
- PESSIMISTIC_WRITE
- PESSIMISTIC_FORCE_INCREMENT
Crie nossa Entidade import lombok.Getter; import lombok.Setter; import javax.persistence.*; @EntityListeners(OperationListenerForMyEntity.class) @Entity public class MyEntity{ @Version private long version; @Id @GeneratedValue @Getter @Setter private Integer id; @Getter @Setter private String value; @Override public String toString() { return "MyEntity{" + "id=" + id + ", version=" + version + ", value='" + value + '\'' + '}'; } }
Vamos criar uma classe na qual todos os métodos de retorno de chamada serão implementados. import javax.persistence.*; public class OperationListenerForMyEntity { @PostLoad public void postLoad(MyEntity obj) { System.out.println("Loaded operation: " + obj); } @PrePersist public void prePersist(MyEntity obj) { System.out.println("Pre-Persistiting operation: " + obj); } @PostPersist public void postPersist(MyEntity obj) { System.out.println("Post-Persist operation: " + obj); } @PreRemove public void preRemove(MyEntity obj) { System.out.println("Pre-Removing operation: " + obj); } @PostRemove public void postRemove(MyEntity obj) { System.out.println("Post-Remove operation: " + obj); } @PreUpdate public void preUpdate(MyEntity obj) { System.out.println("Pre-Updating operation: " + obj); } @PostUpdate public void postUpdate(MyEntity obj) { System.out.println("Post-Update operation: " + obj); } }
Main.java import javax.persistence.*; import java.util.concurrent.*; // , . public class Main { // , .. EntityManagerFactory , . private static EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("ru.easyjava.data.jpa.hibernate"); public static void main(String[] args) { // 10 ( , ). ExecutorService es = Executors.newFixedThreadPool(10); try { // persistFill() - . persistFill(); for(int i=0; i<10; i++){ int finalI = i; es.execute(() -> { // updateEntity(finalI) , java . java - , id, , id , ( , persistFill(), id 500). updateEntity(finalI); }); } es.shutdown(); try { es.awaitTermination(10, TimeUnit.SECONDS); } catch (InterruptedException e) { e.printStackTrace(); } } finally { entityManagerFactory.close(); } } // . private static void updateEntity(int index) { // EntityManager , , . EntityManager em = entityManagerFactory.createEntityManager(); MyEntity myEntity = null; try { em.getTransaction().begin(); // 1. myEntity = em.find(MyEntity.class, 1); // sout, "" . System.out.println("load = "+index); // ( LockModeType.*). em.lock(myEntity, LockModeType.OPTIMISTIC); // Value, , . myEntity.setValue("WoW_" + index); em.getTransaction().commit(); em.close(); System.out.println("--Greeter updated : " + myEntity +" __--__ "+ index); }catch(RollbackException ex){ System.out.println(", =" + myEntity); } } public static void persistFill() { MyEntity myEntity = new MyEntity(); myEntity.setValue("JPA"); EntityManager em = entityManagerFactory.createEntityManager(); em.getTransaction().begin(); em.persist(myEntity); em.getTransaction().commit(); em.close(); } }
Primeira execução com o método updateEntity comentado Pre-Persistiting operation: MyEntity{id=null, version=0, value='JPA'} Post-Persist operation: MyEntity{id=531, version=0, value='JPA'} . id find .
LockModeType.OPTIMISTIC
Este é um bloco
otimista , bem, isso é tão lógico. Como escrevi acima, o valor do campo de versão é comparado; se for diferente, um erro é gerado. Veja isso.
Resultados: Loaded operation: MyEntity{id=531, version=0, value='JPA'} load = 3 Loaded operation: MyEntity{id=531, version=0, value='JPA'} load = 2 Pre-Updating operation: MyEntity{id=531, version=0, value='WoW_2'} Pre-Updating operation: MyEntity{id=531, version=0, value='WoW_3'} Loaded operation: MyEntity{id=531, version=0, value='JPA'} load = 9 Pre-Updating operation: MyEntity{id=531, version=0, value='WoW_9'} Loaded operation: MyEntity{id=531, version=0, value='JPA'} load = 1 Pre-Updating operation: MyEntity{id=531, version=0, value='WoW_1'} Post-Update operation: MyEntity{id=531, version=1, value='WoW_1'} --Greeter updated : MyEntity{id=531, version=1, value='WoW_1'} __--__ 1 , =MyEntity{id=531, version=0, value='WoW_2'} , =MyEntity{id=531, version=0, value='WoW_3'} Loaded operation: MyEntity{id=531, version=1, value='WoW_1'} load = 4 Pre-Updating operation: MyEntity{id=531, version=1, value='WoW_4'} Post-Update operation: MyEntity{id=531, version=2, value='WoW_4'} --Greeter updated : MyEntity{id=531, version=2, value='WoW_4'} __--__ 4 , =MyEntity{id=531, version=0, value='WoW_9'} Loaded operation: MyEntity{id=531, version=2, value='WoW_4'} load = 0 Pre-Updating operation: MyEntity{id=531, version=2, value='WoW_0'} Post-Update operation: MyEntity{id=531, version=3, value='WoW_0'} --Greeter updated : MyEntity{id=531, version=3, value='WoW_0'} __--__ 0 Loaded operation: MyEntity{id=531, version=3, value='WoW_0'} load = 6 Pre-Updating operation: MyEntity{id=531, version=3, value='WoW_6'} Post-Update operation: MyEntity{id=531, version=4, value='WoW_6'} Loaded operation: MyEntity{id=531, version=4, value='WoW_6'} load = 5 Pre-Updating operation: MyEntity{id=531, version=4, value='WoW_5'} Post-Update operation: MyEntity{id=531, version=5, value='WoW_5'} --Greeter updated : MyEntity{id=531, version=4, value='WoW_6'} __--__ 6 --Greeter updated : MyEntity{id=531, version=5, value='WoW_5'} __--__ 5 Loaded operation: MyEntity{id=531, version=5, value='WoW_5'} load = 7 Pre-Updating operation: MyEntity{id=531, version=5, value='WoW_7'} Post-Update operation: MyEntity{id=531, version=6, value='WoW_7'} Loaded operation: MyEntity{id=531, version=5, value='WoW_5'} load = 8 Pre-Updating operation: MyEntity{id=531, version=5, value='WoW_8'} --Greeter updated : MyEntity{id=531, version=6, value='WoW_7'} __--__ 7 , =MyEntity{id=531, version=5, value='WoW_8'}
Observações: Como você pode ver nos resultados, os fluxos 3, 2, 9 e 1 foram os primeiros a carregar e os métodos de retorno de chamada de pré-atualização foram chamados para eles. O primeiro encadeamento no qual o método Post-Update foi chamado foi 1, como você pode ver nos resultados, o campo marcado com a anotação Versão já foi alterado (aumentado em 1). Consequentemente, todos os threads restantes 2, 3, 9 lançaram uma exceção. E assim por diante O resultado é value = WoW_7, versão = 6. De fato, a última pós-atualização foi no encadeamento 7 com a versão = 6.
LockModeType.OPTIMISTIC_FORCE_INCREMENT
Funciona de acordo com o mesmo algoritmo que LockModeType.OPTIMISTIC, exceto que, após a confirmação, o valor do campo Version é aumentado à força em 1. Como resultado, após cada confirmação, o campo aumentará em 2 (o aumento que pode ser visto em Pós-atualização + aumento forçado) . Pergunta Porque Se após uma confirmação, queremos "conjurar" os mesmos dados e não precisamos de transações de terceiros que possam interromper entre a primeira confirmação e o fechamento de nossa transação.
Importante! Se você tentar alterar os dados para o mesmo, nesse caso, os métodos de Pré-atualização e Pós-Atualização não serão chamados. Pode ocorrer o colapso de todas as transações. Por exemplo, várias transações foram lidas ao mesmo tempo, mas, como as chamadas para os métodos pré e pós (atualização) demoram, a transação que tenta alterar os dados (para os mesmos) será executada imediatamente. Isso resultará em um erro nas transações restantes.
LockModeType.PESSIMISTIC_READ, LockModeType.PESSIMISTIC_WRITE e LockModeType.PESSIMISTIC_FORCE_INCREMENT
Como o trabalho dos tipos restantes de bloqueios é semelhante, portanto, escreverei sobre tudo de uma vez e considerarei o resultado apenas por PESSIMISTIC_READ.
LockModeType.PESSIMISTIC_READ - bloqueio de leitura
pessimista .
LockModeType.PESSIMISTIC_WRITE - bloqueio de gravação
pessimista (e lido).
LockModeType.PESSIMISTIC_FORCE_INCREMENT - bloqueio de gravação
pessimista (e lido) com aumento forçado do campo Versão.
Como resultado de tais bloqueios, pode ocorrer uma longa espera por um bloqueio, que por sua vez pode levar a um erro.
Resultado em LockModeType.PESSIMISTIC_READ (não apresentado na íntegra): load = 0 Pre-Updating operation: MyEntity{id=549, version=5, value='WoW_0'} Post-Update operation: MyEntity{id=549, version=6, value='WoW_0'} Loaded operation: MyEntity{id=549, version=6, value='WoW_0'} load = 8 Pre-Updating operation: MyEntity{id=549, version=6, value='WoW_8'} Loaded operation: MyEntity{id=549, version=6, value='WoW_0'} load = 4 Pre-Updating operation: MyEntity{id=549, version=6, value='WoW_4'} ... ERROR: : : 22760 ExclusiveLock " (0,66) 287733 271341"; 20876. 20876 ShareLock " 8812"; 22760.
Como resultado, os threads 4 e 8 se bloquearam, o que levou a um conflito insolúvel. Antes disso, ninguém interferia no segmento 0. Uma situação semelhante com todos os threads até 0.