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]}
Download

Hibernate