Problemas com Concorrência no Hibernate/JPA

18 respostas
AGAraujo

Bom dia pessoal,

Tenho duas classes sendo que uma é a classe “mestre” e a outra “detalhes”. Estas classes estão mapedas corretamente no Hibernate sendo sua relação OneToMany, do mestre ao detalhe.
Até ai nenhum problema, tudo funciona bem exceto quando começo a trabalhar com concorrência.

Segue o código de simulação:

public class Main {

    private static List<ChildEntity> getEntity(MasterEntity me, Integer id, String name){
        ChildEntity c = new ChildEntity(me);
        c.setId(id);
        c.setName(name);
        List<ChildEntity> child = new ArrayList<ChildEntity>();
        child.add(c);
        return child;
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) throws IllegalStateException, SystemException {
        //criando unidade de persistência
        HibernateEntityManagerFactory factory = (HibernateEntityManagerFactory) Persistence.createEntityManagerFactory("poc-hibernatePU");
        //
        HibernateEntityManager em1 = (HibernateEntityManager) factory.createEntityManager();
        HibernateEntityManager em2 = (HibernateEntityManager) factory.createEntityManager();
        //
        Transaction transaction1 = em1.getSession().beginTransaction();
        Transaction transaction2 = em2.getSession().beginTransaction();
        //criando o repository
        MasterRepository repository1 = new MasterRepositoryImpl(em1.getSession());
        MasterRepository repository2 = new MasterRepositoryImpl(em2.getSession());
        //criando um dado mestre
        MasterEntity masterEntity1 = repository1.find();
        MasterEntity masterEntity2 = repository2.find();
        //
        System.out.println("Objeto 1: " + masterEntity1);
        System.out.println("Objeto 2: " + masterEntity2);
        //
        System.out.println("----------antes de qualquer alteração");
        System.out.println(masterEntity1.getName() + " - qtd de itens: " + masterEntity1.getChildEntitys().size());
        System.out.println(masterEntity2.getName() + " - qtd de itens: " + masterEntity2.getChildEntitys().size());
        //
        masterEntity1.setName("MasterEntity1");
        masterEntity1.setChildEntitys(getEntity(masterEntity1, 10, "Child10"));
        //
        System.out.println("----------depois de alteração 1");
        System.out.println(masterEntity1.getName() + " - qtd de itens: " + masterEntity1.getChildEntitys().size());
        System.out.println(masterEntity2.getName() + " - qtd de itens: " + masterEntity2.getChildEntitys().size());
        //
        masterEntity2.setName("MasterEntity2");
        masterEntity2.setChildEntitys(getEntity(masterEntity2, 11, "Child11"));
        //
        System.out.println("----------depois de alteração 2");
        System.out.println(masterEntity1.getName() + " - qtd de itens: " + masterEntity1.getChildEntitys().size());
        System.out.println(masterEntity2.getName() + " - qtd de itens: " + masterEntity2.getChildEntitys().size());
        //
        repository2.update(masterEntity2);
        repository1.update(masterEntity1);
        //cometer os dados do ultimo para o primeiro
        transaction2.commit();
        transaction1.commit();
    }
}

ao executar o console tem este resultado:

Objeto 1: pochibernate.MasterEntity@9c
Objeto 2: pochibernate.MasterEntity@9c

----------antes de qualquer alteração
MasterEntity2 - qtd de itens: 2
MasterEntity2 - qtd de itens: 2

----------depois de alteração 1
MasterEntity1 - qtd de itens: 1
MasterEntity2 - qtd de itens: 2

----------depois de alteração 2
MasterEntity1 - qtd de itens: 1
MasterEntity2 - qtd de itens: 1

As tabelas do banco de dados ficam assim:

Tabela Mestre antes:
Tabela Detalhe antes:
Tabela Mestre depois:
Tabela Detalhe depois:

Assim, gostaria de saber como resolver dois problemas:

  1. A Tabela Mestre não pode ter dados alterados simultaneamente e
  2. A Tabela detalhe que se apresenta com apenas 1 item ao final da operação está inconsistente em relação ao banco de dados.

O que eu posso fazer para resolver estes dois problemas? Ou onde eu posso encontrar algo que trate deste assunto especificamente?

Desde já, obrigado a todos.

18 Respostas

dev.rafael

Kra, eu não consigo ver onde está o seu problema. P/ mim o código parece funcionar perfeitamente!!

AGAraujo

Com certeza o código esteja funcionando corretamente, porém o que desejo que seja realizado não, pois não deve ser possível alterar o objeto Mestre em duas sessões diferentes. Deveria ter sido bloqueado a segunda alteração, pois senão os dados irão ficar inconsistentes.

O outro problema é que a classe mestre inicia o processo com dois itens como é possível ver na imagem 2 e ao final fica com 4 sendo que eu fiz uma substituíção da lista existente no objeto, isto significa que os dados estão inconsistentes, ou seja na proxima recuperação do objeto Mestre este virá com 4 itens ao invés de 1 como fora deixado da ultima vez.

dev.rafael

Na verdade a possibilidade de se editar uma Entidade em duas transações diferentes ao mesmo tempo é bem desejável em sistemas de TI. Mas p/ evitar inconsistências vc deve sempre se lembrar de adicionar um campo @Version nas suas entidades. Esse campo pode ser do tipo Integer ou Timestamp. Isso vai ativar os recursos de Lock Otimista do JPA (no Hibernate não deve ser diferente já q usa o mesmo conjunto de anotações p/ Entidades). O lock otimista fará com q uma entidade q foi atualizada tenha o seu campo @Version atualizado de acordo. Assim somente entidades com um campo @Version compatível com o do banco poderão ser atualizadas, caso contrario uma OptimisticLockException será lançada. Esse mecanismo, contudo, só é interessante em sistemas com baixo índice de concorrência. Em sistema com um índice de concorrência muito auto isso pode promover muitas “colisões”. Nesse caso vc precisará usar o Lock Pessimista, q promove um nível menor de concorrência.

phil.barreto

curti o tema do ubuntu, qual é?

AGAraujo

Opa!!! agora sim.

Bom, já li sobre o Version, mas como você mesmo especificou pode ter muitas colisoes e isto vai me gerar muitas outras situações de controle o que não é o desejavel ou melhor para este projeto.

Sobre o Lock Pessimista não encontrei muita coisa (teoria) sobre isto. Poderia me indicar algo ou me explicar melhor o que realmente ocorre e como controlar as colisoes?

AGAraujo

Estou usando o ubuntu 10.04 para notebook. este tema ver padrão. realmente ficou muito massa!!

phil.barreto

ah sim
é pq eu uso o 9.04, vou atualizar pra usar o tema :smiley:

dev.rafael

Com relação às colisões, isso só quer dizer q vc não deve usar o lock otimista em funcionalidades q serão acessadas por milhares de usuários ao mesmo tempo. Se vc estiver desenvolvendo uma funcionalidade prevendo apenas algumas dezenas de usuário simultâneos então o lock otimista é o mais recomendável. Como melhor controle das colisões eu aconselho q vc capture as OptimisticLockException e atualize dos dados da entidade q está tentando atualizar e mostre esses dados p/ o seu usuário bem como uma mensagem de erro adequada. Pela minha experiência, colisões em Locks Otimistas acontecem pouco na prática, quando eles são usados corretamente. Lembre-se também q vc sempre deve usar o Lock Otimistica mesmo quando vc resolver usar o Lock Pessimista. O Lock Pessimista é bem mais complexo e deve ser usado apenas quando for estritamente necessário. Não é o tipo de recurso q vc vai usar em todas as funcionalidades do seu sistema. Eis aqui um bom artigo sobre lock pessimista no JPA 2.
http://blogs.sun.com/enterprisetechtips/entry/locking_and_concurrency_in_java

AGAraujo

Rafael,

pela sua experiência… veja se estou indo pelo caminho certo.

No meu objeto Mestre há (no sistema real) um atributo que deve ser alterado de forma sincronizada com a inclusao de um objeto Detalhe, sendo que seus dados do mestre são sempre recuperados do banco de dados a cada requisição. Sem concorrência tudo ok.
Porém esta alteração pode ser realizada de forma concorrente (apesar de pouca chance), mas o risco envolvido é de alto impacto (imagine que estamos falando de saldo de conta corrente de clientes e fornecedores que esta na tabela mestre e que os dados inseridos estão na tabela detalhe). Então como já vi na prova de conceito que o Lock Otimista não é bom e não há tempo para tentar o Lock Pessimista ( e segundo sua recomendação, não é aconselhavel para este tipo de sistema), então resolvi criar uma camada mais acima e utilizar um metodo syncronized para resolver o calculo da classe mestre desde sua busca no banco de dados até sua atualização. O problema é que lembro que um professor meu me disse que em alta concorrência pode ser que haja algum furo. Já tentei falar com ele sobre isto e não consegui e por isto estou tentando por aqui.

O que você me diz sobre isto?

dev.rafael

Não é necessário se preocupar com esse tipo de coisa. O lock otimista é o bastante. O lock otimista não vai permitir q os seus dados sejam corrompidos não importa o nível de concorrência. O único problema q vc pode encontrar é, em sistemas com muita concorrência, pode haver um excesso de OptimisticLockException impedindo q as pessoas consigam concluir as operações. Mas só se a quantidade de pessoas atualizando a mesma entidade voi muito grande. De qualquer forma não haverá riscos p/ as suas informações, no máximo um desconforto p/ os seus usuários.

AGAraujo

Rafael,

fiz o teste com Otimistc Lock, funciona, mas o problema é o controle de versionamento que terei que fazer…

o que resolvi foi criar uma camada fina somente para cuidar destes casos, sendo os metodos serializados de suas classes, assim acredito que resolverei a situação e com menos esforço, pois será o único ponto de modificação das classes que precisam deste cuidado. Acredito que funcione!!
Porém, ainda tenho a pulga atrás da orelha com o que meu professor falou.

“Pode haver casos (no inicio) onde métodos sincronizados tenham parte da sua implementação dessincronizadas, em grande quantidade de requisições concorrentes”.

O que você acha?

dev.rafael

O controle de versões das entidades é feito automaticamente pelo JPA. Sempre q uma entidade for atualizada, o campo @Version será atualizado de acordo. Inclusive eu te aconselho não expor essa informação na interface da sua entidade, ou seja, nada de setVersion(). Com relação às situações de atualização e os possíveis conflitos:

public void foo() {
  try {
    utx.begin();
    em.merge(entidade);
    utx.commit();
  }
  catch (OptimisticLockException e) {
    // Assim vc carrega a entidade atualizada do banco.
    entidade = em.find(entidade.getClass(), entidade.getId());

    // ... o tratamento adequado vai aqui.
  }
  catch (Exception e) {
    try {
      utx.rollback();
    }
    catch (Exception e1) {
    }
  }
}

Ou se vc estiver usando EJB (o q é muito melhor):

// EJB
@TransactionAttribute(REQUIRES_NEW)
public void atualizaEntidade() {
  em.merger(entidade);
}

@TransactionAttribute(NEVER)
public String foo() {
  try {
    atualizaEntidade();
  }
  catch (OptimisticLockException e) {
    entidade = em.find(entidade.getClass(), entidade.getId());

    // ... aqui vc trata.
  }
}

Vc não precisa aplicar um synchronized a todo um método. Vc pode:

public void metodoNaoSynchronized() {
  // código não sincronizado...
  synchronized (algumObjectQueNaoPodeSerAcessadoConcorrentemente) {
    // código sincronizado...
  }
  // código não sincronizado...
}

Lembre-se q se vc está apenas acessando informações do banco e, vc tem um EntityManager p/ cada Thread em seu sistema, então vc não precisa de método ou blocos synchronized em seu sistema. O próprio JPA se encarrega disso.

AGAraujo

Rafael,

beleza… vi aqui no livro de JPA sobre o versionamento automatico. Vlw!
Sobre o problema de concorrencia blz tudo funcionando filé.
Veja se consegue me explicar (ajudar) com mais essa.

Veja logo no primeiro post. O objeto Master é alterado para uma lista de apenas 1 elemento, mas no banco ele é acrescido de um elemento, fincando assim incosistente o meu objeto e o meu banco. Veja as imagens 2 e 4 do primeiro post deste topico.

Por que isto acontece? O que fazer?

dev.rafael

Kra, no seu código vc está substituindo a lista de Child. Eu não posso te dizer q esse é o problema, mas é possível q isso só funcione se vc usar o método remove da lista de child ao invés de tentar substitui-la. Outra coisa, ao menos no JPA, vc não precisa usar o método update() explicitamente p/ atualizar um objeto q foi carregado dentro de uma transação. Se a sua entidade foi carregado do banco dentro de uma transação, todas as modificações q vc fizer nesta entidade serão automaticamente aplicadas ao banco quando vc comitar a transação.

AGAraujo

Pois é!! Estou usando Hibernate e achei que o mesmo deveria criar sozinho esta integridade, mas pelo visto preciso fazer um remove depois inserir uma nova lista. Achei q ele faria isto sozinho. Será que o JPA faz, sem precisar fazer um remove no list?

dev.rafael

Creio q não posto q o JPA é implementado p/ provider. De qualquer forma eu acho mais adequado q vc lide com mensagens como add e remove do q setChildEntitys. Add e remove são operações mais semânticas e criam uma interface mais natural e de granularidade mais fina. Quando usa relacionamentos *toMany em minhas entidades do JPA eu sempre evito expor métodos como esse em favor de uma interface com métodos add e remove.

AGAraujo

Vlw cabra.
Quem sabe um dia eu possa retribuir.

Até a próxima e muito obrigado pela experiência.

direisc

dev.rafael, eu estou atrás de uma solução com o lock e esta impossível fazer funcionar não sei se vc pode me ajudar no caso vou tentar explicar a estrutura aqui.
Aplicação WEB pra começar, EJB, JBOSS, JPA2.0, CDI e coisas do tipo (é eu não sei onde eu me meti quando comecei com isso).
Ai a arquitetura esta assim temos um DAO generico que é extendido as necessidades vou reproduzir um trecho:

public class DAOImpl<T, PK> implements DAO<T, PK> { @PersistenceContext(unitName = "SalusPU")//, type=PersistenceContextType.EXTENDED) private EntityManager entityManager; public void persist(T entity) { entityManager.persist(entity); entityManager.flush(); } public void merge(T entity) { entityManager.merge(entity); entityManager.flush(); } public void remove(T entity) { entityManager.remove(entity); }

Os DAOs propriamente ditos:

@Named @Stateful public class AtendimentoDAO extends DAOImpl<Atendimento, Long> {}

os RN (beans que guardam as regras de negocios ou deveriam):

@Named @Stateful public class AtendimentoRN { @Inject private AtendimentoDAO atendimentoDAO; public Atendimento chamarProximo(String tipo, String destino) { String sql = "select a.id from saude.atendimento as a " + " where a.fk_unidade_saude = ? and (a.cor <> 'VERMELHO' or a.cor is null) " + " and a.status_tipo = ? " + " and (a.status = ? or (a.status = ? and a.status_time + interval '5 minutes' <= now()) ) " + " order by a.cor != 'AMARELO', a.cor != 'VERDE', a.cor != 'AZUL', a.cor != 'BRANCO', a.data, a.hora"; String hql = "select a from Atendimento a where a.id = ? and a.status != ? and a.status != ?"; Query consulta = atendimentoDAO.createNativeQuery(sql, contextoBean.getUnidadeAtiva().getId(), tipo, AtendimentoStatus.AGUARDANDO, AtendimentoStatus.A_SER_RECHAMADO).setMaxResults(1); Long idAtendimento; try { idAtendimento = ((Integer) consulta.getSingleResult()).longValue(); consulta = atendimentoDAO.createQuery(hql, idAtendimento, AtendimentoStatus.CHAMADO, AtendimentoStatus.EM_PROCEDIMENTO); Atendimento atendimento = (Atendimento) consulta.getSingleResult(); atendimento = lock(atendimento); try { atendimento.setStatus(AtendimentoStatus.CHAMADO); atendimento.setStatusTime(new Date()); atendimento.setStatusDestino(destino); atendimento.setProfissional(contextoBean.getUsuarioLogado().getProfissional()); salvar(atendimento); return atendimento; } catch (Exception e) { e.printStackTrace(); return null; } } catch (OptimisticLockException e) { return null; } catch (NoResultException nre){ return null; } } public void salvar(Atendimento atendimento) throws Exception { try { if (atendimento.getId() == null || atendimento.getId() == 0) { atendimentoDAO.persist(atendimento); } else { atendimentoDAO.merge(atendimento); } atendimentoStatusRN.log(atendimento); } catch (Exception e) { if (e.getCause() instanceof OptimisticLockException) { throw new Exception("OptimisticLockException!"); } else { throw new Exception("Erro ao salvar atendimento!"); } } }

e outros codigos afins porem não consigo manipular pra que não pare o programa pois qndo acontece o OptimisticLockException o EJB da um EJBTransactionRollback e trava a aplicação tendo que sair e entrar novamente.

tentei achar um meio de por as anotações EJB mas sem muito sucesso tb.

preciso muito de uma ajuda se for possível é claro. Desde ja muito obrigado.

Criado 28 de setembro de 2010
Ultima resposta 29 de ago. de 2013
Respostas 18
Participantes 4