Transações e concorrência Jobson Ronan {[email protected]} O que é uma transação? Uma transação é uma unidade de trabalho que não pode ser dividida. É uma operação atômica. Há dois níveis de granularidade em aplicações corporativas Transações de banco de dados Transações longas (de aplicação): envolvem várias transações de banco de dados Uma transação ou termina com sucesso (commit) ou desfaz todo o processo (rollback) A maior parte da complexidade de se lidar com transações é ocultada pelo sistema (Hibernate, servidor de aplicações, banco de dados) O trabalho consiste, geralmente, em demarcar o início e fim das transações Transações em servidores Demarcar transações em uma aplicação JDBC é fácil. Basta configurar a Conexão conn com da seguinte forma conn.setAutoCommit(false); Os statements executados serão acumulados e só serão tornados definitivos no banco após um conn.commit() Ou serão desfeitos caso ocorra um conn.rollback() Em servidores de aplicação, ou quando é preciso realizar transações entre vários bancos, é preciso usar o protocolo Two-phase commit, que gerencia o processo Para isto existe a API JTA e a classe UserTransaction que encapsula transações distribuídas Tratamento de transações em Hibernate Session session = sessions.openSession(); Transaction tx = null; try { tx = session.beginTransaction(); concludeAuction(); tx.commit(); Executado } catch (Exception e) { if (tx != null) { try { tx.rollback(); } catch (HibernateException he) { //log he and rethrow e } } throw e; } finally { try { session.close(); } catch (HibernateException he) { throw he; } } dentro da transação Transações no Hibernate O Hibernate encapsula o sistema de transações do banco (JDBC) ou servidor (ambiente gerenciado) usado A transação começa na Session com uma chamada para session.beginTransaction() Em um ambiente não gerenciado, isto inicia uma transação JDBC na conexão. Em um ambiente gerenciado, inicia uma transação JTA ou une-se à transação existente. Commit e rollback. Em uma transação Transaction tx: tx.commit() sincroniza o estado da sessão com o banco de dados. tx.rollback() ou desfaz imediatamente a transação ou marca a transação para rollback. É importante fechar a sessão em um bloco finally para garantir que a conexão JDBC será liberada e retornada ao pool de conexões. Flushing (descarregando) O objeto Session implementa "transparent write behind“ Mudanças ao modelo de domínio não são imediatamente persistidas no banco (para reduzir acesso ao banco) Gravação/sincronização transparente dos dados no final da transação. Flushing é a sincronização da camada de objetos com a camada de dados. Ocorre quando Uma transação é cometida Às vezes, antes que uma query é executada Quando a aplicação chama explicitamente session.flush() Isolamento Bancos de dados e sistemas transacionais tentam garantir isolamento entre transações Bancos de dados fornecem vários graus de flexibilização de isolamento Isolamento completo é uma utopia. É muito caro em termos de escalabilidade da aplicação. Variam de isolamento completo a isolamento praticamente inexistente (neste caso, cabe à aplicação lidar com os conflitos) Para a maior parte das aplicações, isolamento incompleto de uma transação é aceitável Problemas de isolamento O padrão ANSI SQL define os níveis de isolamento de transações em termos de fenômenos que podem ou não serem permitidos. Os fenômenos são: Update perdido (lost update) Duas transações ambas atualizam um registro e a segunda transação aborta, fazendo com que as duas mudanças sejam perdidas. As transações concorrentes não têm isolamento algum. Leitura suja (dirty read) Uma transação lê mudanças feitas por transação que ainda não cometeu os dados. Essa mudança pode ser desfeita em um rollback. Problemas de isolamento Leitura não-repetível (unrepeatable read) Uma transação lê um registro duas vezes e obtém um estado diferente em cada leitura. Outra transação pode ter gravado dados e cometido mudanças entre as duas leituras Leitura fantasma (phantom read) Uma transação executa uma consulta duas vezes, e o segundo resultado inclui registros que não estavam na primeira consulta. Novos registros foram inseridos por outra transação entre as consultas. Níveis de isolamento JDBC JTA usa esses mesmos níveis de isolamento Read uncommitted Permite dirty reads mas não updates perdidos. Uma transação não pode gravar em um registro se outra transação não cometida já gravou dados nele. Este nível de isolamento pode ser implementado com locks de gravação exclusiva. Read committed Permite unrepeatable reads mas não dirty reads. Transação de gravação não cometida impede que outras transações acessem registro. Transações de leitura não bloqueiam o sistema. Níveis de isolamento JDBC Repeatable read Não permite unrepeatable reads nem dirty reads. Podem ocorrer phantom reads. Transações de leitura bloqueiam transações de gravação (mas não outras transações de leitura) e transações de gravação bloqueiam todas as outras. Serializable Fornece o isolamento mais rigoroso. Emula execução em série de transações (em vez de concorrentemente). Qual nível de isolamento? A escolha do nível de isolamento depende do cenário onde a aplicação executa. Qual um nível razoável de isolamento para aplicações típicas? Não existe uma regra que sirva para todas as situações. Isolamento excessivo geralmente não é aceitável, devido ao alto custo quanto à escalabilidade (crítica nas aplicações típicas do Hibernate), portanto o isolamento serializable não deve ser usado O isolamento read uncommitted é perigoso, e não deve ser usado se houver opções melhores no banco Suporte a versioning (travas otimistas) e uso do cache de segundo nível (por classe) do Hibernate já alcançam a maior parte dos benefícios de um isolamento do tipo repeatable read, usando read committed. Portanto, read committed é uma boa opção com o Hibernate. Como mudar o nível de isolamento default? É preciso definir uma propriedade no hibernate.properties ou hibernate.cfg.xml. Use: hibernate.connection.isolation=numero onde número é 1, 2, 4 ou 8. Exemplo: hibernate.connection.isolation = 4 O número refere-se a um dos quatro níveis: 1—Read uncommitted isolation 2—Read committed isolation 4—Repeatable read isolation 8—Serializable isolation Só é possível fazer esse controle em ambientes não gerenciados Servidores de aplicação têm configuração própria. Estratégias de isolamento locais Nível de isolamento global afeta todas as conexões Read committed é um bom isolamento default para aplicações Hibernate Mas pode ser desejável utilizar travas mais rigorosas para transações específicas Existem duas estratégias Travas pessimistas (evita colisões entre transações bloqueando totalmente o acesso de outras transações) Travas otimistas (onde o sistema flexibiliza o isolamento mas lida com eventuais colisões) Travas pessimistas Uma trava pessimista é adquirida quando dados são lidos e mantidos isolados de outras transações até que a sua transação complete. Classe LockMode Em modo read-committed, o banco de dados nunca adquire travas pessimistas a não ser que sejam requisitadas explicitamente Permite a solicitação de uma trava pessimista em um objeto Considere a seguinte transação Transaction tx = session.beginTransaction(); Category cat = (Category) session.get(Category.class, catId); cat.setName("New Name"); tx.commit(); Uma trava pessimista pode ser obtida da seguinte forma: Transaction tx = session.beginTransaction(); Category cat = (Category) session.get(Category.class, catId, LockMode.UPGRADE); cat.setName("New Name"); tx.commit(); Controle de LockMode Os modos suportados para LockMode são: NONE - Só vai ao banco se o objeto não estiver no cache. Default em load() e get() READ - Ignora cache e faz verificação de versão para assegurar-se que o objeto na memória é o mesmo que está no banco. UPDGRADE - Ignora cache, faz verificação de versão (se aplicável) e obtém trava pessimista (se suportada). UPDGRADE_NOWAIT - Mesmo que UPGRADE, mas desabilita a espera por liberação de travas, e provoca exceção de locking se a trava não puder ser obtida. WRITE - Obtida automaticamente quando Hibernate grava em um registro na transação atual Controle de LockMode Sincronização de objeto desligado se registro não foi alterado por outra transação. Item item = ... ; Bid bid = new Bid(); item.addBid(bid); ... Transaction tx = session.beginTransaction(); session.lock(item, LockMode.READ); tx.commit(); Caching é considerada uma solução melhor que travas pessimistas. Evite usar LockMode explícito a não ser que realmente seja necessário. Transações longas (de aplicação) Processos de negócio Podem ser consideradas uma única unidade de trabalho do ponto de vista de um usuário. Transação de baixa granularidade. Uma noção mais abrangente da unidade de trabalho. Exemplo de cenário típico 1) 2) 3) Dados são recuperados e mostrados na tela em uma primeira transação do banco O usuário tem uma oportunidade de visualizar e modificar os dados, fora de uma transação As modificações são feitas persistentes em uma segunda transação de banco de dados Como lidar com as colisões? Três estratégias A primeira opção é problemática para várias aplicações Último commit ganha - os dois updates funcionam, mas o segundo sobrescreve as alterações do primeiro. Nenhuma mensagem de erro é mostrada. Primeiro commit ganha - a primeira modificação é feita persistente, e o usuário que envia a segunda recebe uma mensagem de erro. Optimistic locking. Mesclar updates conflitantes - A primeira modificação é persistida, e a segunda pode ser aplicada seletivamente pelo usuário. É importante que o usuário pelo menos saiba do erro Acontece por default. Hibernate ajuda a implementar as outras duas estratégias usando controle de versões e travas otimistas. Uso de managed versioning (controle de versão) Depende de que um número seja incrementado sempre que um objeto é modificado. public class Comment { ... private int version; ... void setVersion(int version) {this.version = version;} int getVersion() {return version;} } No arquivo de mapeamento, <version> vem logo depois de <id> <class name="Comment" table="COMMENTS"> <id ... <version name="version" column="VERSION"/> ... </class> O número de versão é só um contador. Não tem outra utilidade. Uma alternativa é usar um timestamp Timestamp Alternativa ao <version>. Exemplo: public class Comment { ... private Date lastUpdated; void setLastUpdated(Date lastUpdated) { this.lastUpdated = lastUpdated; } public Date getLastUpdated() {return lastUpdated;} } Mapeamento <class name="Comment" table="COMMENTS"> <id ...../> <timestamp name="lastUpdated" column="LAST_UPDATED"/> ... </class> Em tese, um timestamp é menos seguro pois duas transações concorrentes poderiam tentar load e update no mesmo milisegundo. Travas otimistas O Hibernate controla a inicialização e gerenciamento de <version> e <timestamp> automaticamente. Esses recursos permitem o eficiente gerenciamento de colisões que implementam a estratégia de trava otimista. StaleObjectStateException é lançado em caso de inconsistência Otimistas versus Pessimistas Enfoque pessimista assume que serão constantes os conflitos e o ideal é bloquear completamente o acesso. Não ultrapassa os limites de uma sessão Enfoque otimista assume que conflitos serão raros e quando eles acontecerem, é possível lidar com eles. Garante maior escalabilidade e suporta transações longas. Granularidade de uma Sessão Session-per-request Session-per-request-withdetached-objects Uma sessão tem a mesma granularidade de uma transação Objetos são modificados entre duas sessões Uma transação por sessão Objetos desligados Session-per-applicationtransaction Sessão longa Objetos mantêm-se persistentes Cache O cache é uma cópia local dos dados. O cache evita acesso ao banco sempre que Fica entre sua aplicação e o banco de dados. A aplicação faz uma pesquisa por chave primária ou A camada de persistência resolve uma associação usando estratégia lazy Podem ser classificados quanto ao escopo: Escopo de transação - cada unidade de trabalho tem seu próprio cache; vale enquanto a transação está rodando. Escopo de processo - o cache é compartilhado entre transações (há implicações quanto ao isolamento) Escopo de cluster - compartilhado entre processos na mesma máquina ou entre múltiplas máquinas de um cluster. Cache no Hibernate Dois níveis Primeiro nível tem escopo de transação. Segundo nível é opcional e tem nível de processo ou cluster. O primeiro nível é a Session. Uma session ou tem a duração de uma transação de banco de dados ou de uma transação de aplicação longa. Não pode ser desligada. Garante identidade do objeto dentro da transação. O segundo nível é cache de estado (valores; não instâncias) É opcional Pode ser configurado por classe ou por associação. Primeiro e segundo cache Cache de primeiro nível Automático (Session) Usado sempre que se passa um objeto para save(), update(), saveOrUpdate() ou quando ele é requisitado com load(), find(), list(), iterate(), ou filter() Garante que quando uma aplicação requisita o mesmo objeto persistente duas vezes numa sessão, ela recebe de volta a mesma instância. Cache de segundo nível Instâncias persistentes são desmontadas (é como serialização, mas o algoritmo é mais rápido). Requer conhecimento sobre os dados para uso eficiente (não é automático – as classes são mapeadas ao cache uma por uma) Se dados são mais freqüentemente atualizados que lidos, não habilite o cache de segundo nível Requer configuração fina em gerente de cache para melhor performance Resumo: tags de mapeamento <version> Usado em implementação de transações longas, para sinalizar que uma tabela/objeto está sendo alterada <cache> Usado para definir política de cache de segundo nível Propriedades: transação e cache hibernate.cache.provider_class=nome.da.Classe Usa um cache provider próprio em substituição ao nativo usado pelo Hibernate (implementação de org.hibernate.cache.CacheProvider) hibernate.transaction.factory_class=nome.da.Classe Para definir um gerente de transações próprio, ou org.hibernate.transaction.<nome> para usar uma implementação disponível, onde <nome> pode ser JBossTransactionManagerLookup WeblogicTransactionManagerLookup WebSphereTransactionManagerLookup OrionTransactionManagerLookup ResinTransactionManagerLookup JOTMTransactionManagerLookup JOnASTransactionManagerLookup ... Propriedades para hibernate.properties ou hibernate.cfg.xml Boas Práticas Usar sempre o padrão facade ...mas como englobar várias operações a a vários DAOs em uma transação? Classe utilitária simples public class HibernateUtil { private static final SessionFactory sessionFactory; static { try { Configuration cfg = new Configuration(); sessionFactory = cfg.configure().buildSessionFactory(); } catch (Throwable ex) { ex.printStackTrace(System.out); throw new ExceptionInInitializerError(ex); } } //... } Classe utilitária simples //.. private static Session session; public static Session getSession() { try { if (session == null || !session.isOpen()) { SessionFactory factory = getSessionFactory(); session = factory.openSession(); } return session; } catch (Exception e) { throw new RuntimeException(e); } } //... Classe utilitária simples Suporte transacional //.. private static Transaction transaction; public static void beginTransaction() { transaction = getSession().beginTransaction(); } public static void commit() { if (transaction != null) transaction.commit(); } public static void rollback() { if (transaction != null) transaction.rollback(); } //... Usando Todo DAO, quando precisar de uma Sessão, irá obte-la através do getSession() A fachada pode gerencar a transação com os metodos begin, commit e rollback Para que vários DAOs obtenham a mesma sessão, basta não fecha-la Os DAOs não gerencia, mais as transações ...Mas se a houver acesso concorrente Classe utilitária com suporte a concorrencia //.. private static final ThreadLocal<Session> localSession = new ThreadLocal<Session>(); public static Session getSession() { try { Session session = localSession.get(); if (session == null || !session.isOpen()) { SessionFactory factory = getSessionFactory(); session = factory.openSession(); localSession.set(session); } return session; } catch (Exception e) { throw new RuntimeException(e); } } //... Classe utilitária com suporte a concorrencia //.. private static final ThreadLocal<Transaction> localTx = new ThreadLocal<Transaction>(); public static void beginTransaction() { localTx.set(getSession().beginTransaction()); } public static void commit() { if (localTx.get() != null) localTx.get().commit(); } public static void rollback() { if (localTx.get() != null) localTx.get().rollback(); } //... Exercício Criar o modelo de Objetos analisando o schema do banco legado script.sql Criar criar DAOs e fachada para a aplicação Criar methodos de negício para Realizar uma reserva Agendar uma reserva Cancelar uma reserva Transações e concorrência Jobson Ronan {[email protected]}