Problemas com Concorrência no Hibernate/JPA

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.

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

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.

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.

curti o tema do ubuntu, qual é?

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?

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

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

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

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?

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.

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?

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.

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?

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.

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?

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.

Vlw cabra.
Quem sabe um dia eu possa retribuir.

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

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.