Transações e concorrência
Jobson Ronan
{jrjs@cin.ufpe.br}
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();
} 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; } }
Executado 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
Isolamento completo é uma utopia.
É muito caro em termos de escalabilidade da aplicação.
Bancos de dados fornecem vários graus de flexibilização de isolamento
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.
Não existe uma regra que sirva para todas as situações.
Qual um nível razoável de isolamento para aplicações típicas?
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.
Em modo read-committed, o banco de dados nunca adquire travas pessimistas a não ser que sejam requisitadas
explicitamente
Classe LockMode
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) Dados são recuperados e mostrados na tela em uma primeira transação do banco
2) O usuário tem uma oportunidade de visualizar e modificar os dados, fora de uma transação
3) 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
Ú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.
A primeira opção é problemática para várias aplicações
É 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
Uma sessão tem a mesma granularidade de uma
transação
Session-per-request-with- detached-objects
Objetos são modificados entre duas sessões
Uma transação por sessão
Objetos desligados
Session-per-application- transaction
Sessão longa
Objetos mantêm-se persistentes
Cache
O cache é uma cópia local dos dados.
Fica entre sua aplicação e o banco de dados.
O cache evita acesso ao banco sempre que
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.prop erties ou
hibernate.cfg.x ml
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()
Para que vários DAOs obtenham a mesma sessão, basta não fecha-la
A fachada pode gerencar a transação
com os metodos begin, commit e rollback
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