MokaByte 54 - Luglio/Agosto 2001
Foto dell'autore non disponibile
di
Giovanni Puliti
Corso di EJB
parte VII: la gestione delle transazioni
Uno degli aspetti più importanti lasciati in sospeso fino ad ora è sicuramente quello relativo alla gestione delle transazioni per le operazioni di accesso ai dati durante il normale ciclo di vita di un EJB. Questo mese vediamo come si gestisce questo importante e delicato aspetto


Dopo gli articoli dei mesi scorsi si dovrebbe essere in possesso adesso delle conoscenze di base della tecnologia EJB, anche se ad ora però ne è stato volutamente lasciato in sospeso uno degli aspetti più importanti di EJB, ovvero quello relativo alla gestione delle transazioni.
Per la stessa natura degli EJB è piuttosto evidente che l’accesso concorrente da parte di più client sullo stesso set di bean comporta prima o poi un accesso concorrente ai dati memorizzati nel database, rendendo necessario un qualche sistema di controllo dei dati.

Visto che la filosofia di base di EJB è quella di semplificare il lavoro dello sviluppatore di bean, anche in questo caso il lavoro “sporco” verrà effettuato dal container, consentendo una volta di più di lasciare il programmatore a concentrarsi solo sulla logica business del componente.
Vediamo quindi quali sono i passi necessari per fornire il cosiddetto supporto transazionale ai componenti EJB.
 
 
 

Introduzione alle transazioni: il modello ACID
L’obiettivo primario di un sistema transazionale è garantire che l’accesso concorrente  ai dati non porti a situazioni incoerenti sui dati stessi o sui risultati di tali operazioni. Per ottenere questo risultato in genere si fa riferimento al cosiddetto modello ACID, ovvero una transazione deve essere atomica (Atomic), consistente (Consistent), isolata (Isolated), e duratura (Durable).

Atomic indica che  tutte le operazioni che costituiscono la transazione devono essere eseguite senza interruzioni; se per un qualche motivo una qualsiasi delle operazioni dovesse fallire, allora il motore transazionale dovrà ristabilire la situazione originaria prima che la prima operazione della transazione sia stata eseguita. Nel caso di transazioni sui database, questo significa che ai dati dovranno essere rassegnati i valori iniziali precedenti all’inizio della transazione.
Se invece al contrario tutte le operazioni sono state eseguite con successo, allora le modifiche sui dati nel database potranno essere effettuate realmente. 

La Consistenza dei dati è invece un obiettivo che si ottiene grazie al lavoro congiunto del sistema transazionale e dello sviluppatore: nel primo caso si ottiene questo sia grazie all’utilizzo di atomicità  ed isolamento sia tramite i controlli sulle relazioni fra le tabelle del database inseriti nel database engine;  lo sviluppatore invece dovrà progettare le varie operazioni di business logic in modo  da garantire la consistenza ovvero l’integrità referenziale, correttezza delle chiavi primarie, e così via.

L’Isolamento garantisce che la transazione verrà eseguita dall’inizio alla fine senza l’interferenza di elementi esterni o di altri soggetti.

La Durabilità infine deve garantire che le modifiche temporanee ai dati debbano essere effettuate in modo persistente in modo da evitare che un eventuale crash del sistema possa portare alla perdita di tutte le operazioni intermedie.
 
 
 

Lo Scope transazionale
Il concetto di scope transazionale è di fondamentale importanza nel mondo EJB, ed indica l’insieme di quei bean che prendono parte ad una determinata transazione. Il termine scope viene utilizzato proprio per dar risalto al concetto di spazio di esecuzione: infatti ogni volta che un bean facente parte di un determinato scope invoca i metodi di un altro o ne ricava un qualche riferimento causa  l’inclusione di quest’ultimo nello stesso scope a cui appartiene lui stesso. 
In tal modo quando il primo bean transazionale prende vita, lo scope verrà propagato a macchia d’olio a tutti i bean interessati dalla esecuzione. 
Anche se può essere piuttosto semplice seguire la propagazione dello scope monitorando  il  thread di esecuzione di un bean,  in realtà, come si avrà modo di vedere più in dettaglio in seguito,  la propagazione dello scope si interseca con la politica definita in fase di deploy per ogni singolo bean, dando così vita ad uno scenario piuttosto complesso. 
La gestione della transazionalità di un bean e quindi la modalità con cui esso potrà prendere parte ad un determinato scope (sia attivamente che perché invocato da altri bean) può essere gestita in modo automatico dal container in base ai valore dei vari parametri transazionali, oppure manualmente nel caso in cui si faccia esplicitamente uso di un sistema sottostante come Java Transction Api (JTA). Normalmente, a meno di particolari esigenze, non è necessario gestire direttamente le transazioni, ma ci si affida al container di EJB: la capacità di poter specificare come i vari componenti possono prendere parte alle varie transazioni in atto è una delle caratteristiche più importanti del modello EJB, fin dalla specifica 1.0.
 
 


Tabella 1: gli attributi transazionali in EJB 1.0 ed 1.1

Nel caso in cui ci si voglia affidare al motore transazionali fornito dal container EJB, si può facilmente definire il comportamento di un determinato bean facendo uso degli attributi transazionali elencati nella tabella 1. 
Il valore di tali attributi è cambiato dalla versione 1.0 alla 1.1: se prima infatti si faceva riferimento a costanti contenute nella classe ControlDescriptor, nella 1.1 si è passati a più comode stringhe di testo, scelta sicuramente che permette di utilizzare file XML per la configurazione manuale del bean.
Tramite gli attributi riportati nella tabella 1 è possibile definire il comportamento sia per tutto il bean che per ogni singolo metodo, cosa quest’ultima che sebbene sia più complessa ed a rischio di errori,  offre sicuramente maggior controllo e potenza.
In EJB 1.0 per poter impostare uno dei possibili valori transazionali è necessario scrivere del codice Java apposito seguendo la stessa tecnica vista in precedenza: ad esempio per prima cosa è necessario definire un ControlDescriptor impostando opportunamente l’attributo transazionale

ControlDescriptor cd= new ControlDescriptor();
cd.setMethod(null);
cd.setTransactionAttribute(ControlDescriptor. TX_ NOT_SUPPORTED);
ControlDescriptor []cds= {cd};
SessionDes.setControlDescriptors(cds);

dove SessionDes è un SessionDescriptor del bean, da utilizzarsi come si è visto in precedenza. In questo caso con cd.setMethod(null) si imposta l’attributo transazionale per tutto il bean (valore null), mentre se si fosse desiderato impostare tale valore solo per un metodo si sarebbe dovuto tramite la reflection, individuare il metodo esatto e passarlo come parametro. Ad esempio

Class []parameters= new Class[0];
Method method=MyBean.class.getDeclaredMethod(“getName”,parameters);
cd.setMethod(method);

e poi procedere come in precedenza.
In EJB 1.1 invece le cose sono sicuramente più semplici, infatti tramite un file XML è possibile ottenere lo stesso risultato. Ad esempio, riconsiderando l’esempio visto in precedenza, tramite il seguente codice XML 

<container-transaction>
  <method>
    <ejb-name>BMPMokaUser</ejb-name>
    <method-name>*</method-name>
  </method>
  <trans-attribute>Required</trans-attribute>
</container-transaction>
 

definiamo che tutti i metodi del bean BMPMokaUser avranno l’attributo Required.
 

Significato dei valori transazionali
Si vedranno adesso i vari valori transazionali utilizzando per semplicità il loro nome in italiano, intendendo sia le costanti di EJB 1.0 che le stringhe di testo della versione 1.1.
 

NOTA volte si usa dire che una determinata transazione  client è sospesa, ad indicare che la transazione del client non è propagata al metodo del bean invocato,  e la transazione è temporaneamente sospesa fino a che il metodo invocato non termina. Si ricordi che come detto altrove per client si intende una applicazione stand alone o anche semplicemente un altro bean. 
 

Not Supported
Invocando all’interno di una transazione un  metodo di un bean settato con questo valore, si otterrà una interruzione della transazione;  lo scope della transazione non verrà propagato al bean o ad altri da lui invocati. Appena il metodo invocato termina, la transazione riprenderà la sua esecuzione.


Figura 1: il funzionamento dell’attributo transazionale not supported. In questo caso 
lo scope della transazione non verrà propagato al bean o ad altri da lui invocati 




Supports
Nel caso in cui il metodo di un bean sia impostato a questo valore, allora la sua invocazione da parte di un client  facente parte già di un determinato scope, provocherà la propagazione dello scope al metodo. Ovviamente non è necessario che il metodo sia necessariamente invocato all’interno di uno scope, per cui potranno invocarlo anche client non facenti parte di nessuna transazione.


Figura 2 Confgurazione Supports: in  questo caso il bean 
è in grado di entrare nello scope transazionale del client, anche 
se può essere invocato al di fuori di uno scope.


Required
In questo caso si ha la necessità della  presenza di uno scope per l’invocazione del metodo. Nel caso in cui il client sia parte di una transazione, allora lo scope verrà propagato, altrimenti uno nuovo verrà creato appositamente per il metodo del bean (scope che verrà terminato al termine del metodo). 
 



Figura 3 Confgurazione  Required: un bean di questo tipo deve 
essere eseguito obbligatoriamente all’interno di uno scope: se il client 
opera in uno scope, il bean entrerà a far parte di quello del client oppure
 altrimenti un nuovo scope verrà creato appositamente per il bean




Requires New
In questo caso il bean invocato da vita necessariamente ad una nuova transazione, indipendentemente dal fatto che il client faccia parte o meno di  una transazione. Nel caso in cui il client sia parte di una transazione, quest’ultima verrà interrotta fino al completamento della transazione del bean invocato. Il nuovo scope creato per il bean verrà propagato esclusivamente a tutti i bean invocati dal bean di partenza. 
Quando il bean invocato terminerà la sua esecuzione, il controllo ritornerà alla client che riprenderà la sua transazione.
 



Figura 4 Confgurazione Requires New: in questa configurazione 
il bean creerà sempre un suo nuovo scope




Mandatory
Questo valore serve per specificare che il bean deve sempre essere parte di una transazione; nel caso in cui il client invocante non appartenga a nessuno scope transazionale, il metodo del bean genererà una eccezione TransactionRequiredException.


Figura 5 Confgurazione  Mandatory: è una situazione simile alla  required, 
anche se in questo caso la mancanza di uno scope preesistente nel client 
provoca la generazione di una eccezione da parte del bean, e non la 
creazione di uno scope apposito




Never (solo in EJB 1.1)
In questo caso il client invocante non può appartenere a nessun scope transazionale, altrimenti il bean invocato genererà una RemoteException.


Figura 6 Confgurazione  Never:l’esatto contrario del caso precedente. 
In questo caso il bean non può assolutamente appartenere ad uno 
scope, pena la generazione di una eccezione.





Bean Managed (solo in EJB 1.0)
In questo caso, possibile solo con la specifica 1.0, si  indica che nessun supporto per la transazione deve essere specificato, dato che la gestione verrà effettuata manualmente (tramite ad esempio JTS) all’interno dei metodi del bean. 


Figura 7  Confgurazione Bean Managed: il bean opererà sempre 
in un suo scope personale, gestito direttamente dal bean tramite 
un transaction engine come JTS.




I vari attributi possono essere impostati in modo autonomo sui vari metodi del bean, ad eccezione di quest’ultimo caso, dove l’impostazione a bean managed per anche uno solo dei metodi obbliga necessariamente a fornire il  supporto manuale per tutti i metodi del bean.
 
 
 

Approfondimenti sul database e integrità dei dati
Il concetto forse più importante e più critico di un sistema transazionale concorrente  è  quello relativo all’isolamento (lettera I del modello ACID), dato nasconde alcune importanti insidie. 
Il livello di isolamento di una transazione in genere è valutabile in funzione della qualità con cui riesce a risolvere i seguenti problemi:

  1. Dirty reads: si immagini il caso in cui due transazioni una in lettura ed una in scrittura, debbano accedere ai medesimi dati. In questa situazione si possono avere incoerenze dei dati, nel caso in cui la transazione in lettura accedesse ai dati appena modificati da quella in scrittura, quando quest’ultima dovesse per un motivo qualsiasi effettuare un rollback riponendo i dati nella configurazione originaria.
  2. Repeteable reads: questa condizione garantisce l’immutabilità dei dati al succedersi di differenti letture all’interno della stessa transazione. In genere questa condizione è garantita o tramite un lock sui dati, oppure tramite l’utilizzo di copie dei dati  in memoria su cui effettuare le modifiche. In genere la prima soluzione è più sicura, anche se impatta pesantemente sulle prestazioni. La seconda invece può complicare molto la situazione, a causa delle difficoltà derivanti dalla necessità di sincronizzare i dati copia con  quelli originali. Una lettura non ripetibile si ha quando una transazione dopo una prima lettura, vedrà alla seguente le modifiche effettuate dalle altre transazioni.
  3. Phantom reads: letture di questo tipo possono verificarsi quando nuovi dati aggiunti al database sono visibili  anche all’interno di transazioni iniziate prima dell’aggiunta dei dati al database, transazioni che quindi si ritrovano nuovi dati la cui presenza è apparentemente immotivata.


Ecco alcune soluzioni comunemente utilizzate per prevenire i problemi di cui sopra:

  1. Read locks: questo blocco impedisce ad altre transazioni di modificare i dati quando una determinata transazione effettua una lettura dei dati. Questo previene l’insorgere di letture non ripetibili, dato che le altre transazioni possono leggere i dati ma non modificarli o aggiungerne di nuovi. Se il lock venga effettuato su un record, su tutta la tabella oppure  su tutto database dipende dalla implementazioni particolare del database.
  2. Write locks: in questo caso, utilizzato in operazioni di aggiormanto, le altre transazioni sono impedite di effettuare modifiche ai dati; rappresenta un livello di sicurezza ulteriore, anche se non impedisce l’insorgere di letture sporche dei dati (dirty reads) da parte di altre transazioni ed anche di quella in corso.
  3. Exclusive locks: questo è il blocco più restrittivo ed impedisce ad altre transazioni di effettuare letture o scritture sui dati bloccati: le dirty e phantom reads quindi non possono verificarsi.


Alcuni sistemi offrono un meccanismo alternativo ai lock, detto comunemente snapshot di dati: in questo caso sono create delle vere e proprie istantanee dei dati effettuate al momento dell’inizio della transazione, istantanee che permettono di lavorare in lettura e scrittura su una copia dei dati. Se questo elimina del tutto il problema dell’accesso concorrente, introduce problemi non banali relativamente alla sincronizzazione dei dati reali con le varie snapshot.
 
 
 

Livelli di isolamento delle transazioni
Per qualificare la bontà di un sistema transazionale in genere si fa riferimento ai cosiddetti livelli di isolamento.

Read Uncommitted: una transazione può leggere tutti i dati uncommitted (ovvero quelli ancora non resi persistenti)  di altre transazioni in atto. Corrisponde al livello di garanzia più basso dato che può dar vita a dirty e phantom reads, cosi come possono verificarsi letture non ripetibili.

Read Committed: una transazione non può leggere i  dati temporanei (not committed) di altre transazioni in atto. Sono impedite le dirty reads, ma possono verficarsi le letture fantasma e le non ripetibili. I metodi di un bean con questo livello di isolamento non possono leggere dati affetti da una transazione.

Repeteable reads: una transazione non può modificare i dati letti da un’altra transazione. Sono impedite  le dirty reads e le letture fantasma ma possono verficarsi le letture non ripetibili.

Serializable: corrisponde al livello massimo di sicurezza, dato che una determinate transazione ha l’esclusivo diritto di accesso in lettura e scrittura sui  dati. Si ha la garanzia contro le le dirty reads, le letture fantasma e le non ripetibili. 

Questi attributi sono gli stessi definiti come costanti in JDBC all’interno della classe java.sql.Connection.
 
 
 

Gestione del livello di isolamento in EJB
A parte il caso della gestione diretta all’interno del bean delle transazioni, la specifica 1.1 non prevede un sistema per l’impostazione tramite attributi del livello di isolamento come invece accadeva per la 1.0.  In questo caso l’isolamento può essere scelto fra quattro livelli ed assegnato ai vari metodi grazie come al solito ad un oggetto ControlDescriptor. Ecco ad esempio una breve porzione di codice che mostra questa semplice operazione

ControlDescriptor cd = new ControlDescriptor();
cd.setIsolationLevel(ControlDescriptor.TRANSACTION_SERIALIZABLE);
cd.setMethod(null);
ControlDescriptor []cds = {cd};
SessionDes.setControlDescriptors(cds);

In questo caso si è scelto il livello TRANSACTION_SERIALIZABLE, mentre nella tabella 2 sono riportati tutti i valori disponibili

Livello di isolamento Costanti di ControloDescriptor
Read committed TRANSACTION_READ_COMMITTED
Read Uncommitted TRANSACTION_READ_UNCOMMITTED
Repeatable reads TRANSACTION_REPEATABLE_READ
Serializable TRANSACTION_SERIALIZABLE
 

In EJB 1.0 è possibile impostare livelli di isolamento differenti per ogni metodo, anche se poi in fase di runtime è necessario che all’interno della medesima transazione siano invocati metodi con lo stesso livello di isolamento.
 
 
 

Scelta del livello di isolamento: il giusto compromesso
Visti i molti problemi legati alla gestione concorrente dei dati, si potrebbe pensare che la soluzione migliore possa essere l’adozione sempre e comunque di un livello di isolamento molto alto, al fine di assicurare in ogni istante la correttezza dei dati e delle operazioni svolte su di essi.
Come si è avuto modo già di accennare in precedenza questa non sempre è la soluzione migliore, visto che è vera la regola empirica che vede inversamente proporzionali il livello di isolamento e le performance complessive del sistema. 
Infatti, oltre ad un maggiore numero di operazioni di controllo da effettuare, l’accesso esclusivo in lettura, scrittura o entrambi trasforma lo scenario da concorrente a sequenziale. 
Purtroppo non esistono regole precise  per poter scegliere in modo preciso e semplice quale sia la soluzione migliore da adottare in ogni circostanza e spesso ci si deve basare sull’esperienza valutando in base al contesto in cui si sta operando.
Ad esempio se si è  in presenza di un bean che potenzialmente possa essere acceduto da molti client contemporaneamente (è il caso di bean che rappresentano una entità centrale, come la banca nell’esempio delle transazioni monetarie visto in precedenza), allora non sarà molto saggio utilizzare un livello alto di isolamento, dato che questo metterà in coda tutte le chiamate di tutti i client.
E’ altresì vero che, proprio per l’elevato numero di client che accederanno al bean comune, si dovrà fornire un livello di sicurezza molto alto, dato che un piccolo errore si potrebbe ripercuotere su un numero elevato di applicazioni client.
La soluzione migliore potrebbe essere quella di separare i vari contesti e di applicare differenti livelli di isolamento per i vari metodi. Ad esempio tutti i  metodi che dovranno essere invocati spesso per ottenere informazioni dal bean, ovvero metodi che corrispondono ad operazioni di lettura, potranno essere impostati con un livello molto basso di isolamento (sia perché non si rende necessario altrimenti sia per favorire le prestazioni) come ad esempio Read Uncommitted. I metodi del tipo setXXX potrebbero essere invocati molto di rado (è probabile che la banca non cambierà mai il suo nome), e quindi in  questo caso deve essere fornito il livello massimo di isolamento (in questo caso le prestazioni non sono un problema dato lo scarso uso dei metodi, mentre la correttezza deve essere massima).
Ovviamente in tutti i casi intermedi è necessario pensare se sia il caso di introdurre qualche funzionalità di business logic ad hoc per sopperire ad un livello di sicurezza minore. 
 

Conclusione
Abbiamo affrontato con questo articolo il tema conclusivo relativamente alla piattaforma EJB. Il prossimo mese concluderemo affrontando il caso della gestione manuale delle transazioni. 
Come si è potuto vedere gli argomenti sono piuttosto delicati e generici. Solo con l’uso e l’esperienza se ne potranno comprendere a fondo i diversi aspetti. Una volta di più, grazie alla trattazione delle transazioni, si è potuto vedere come il modello EJB sia potente ci risparmi allo sviluppatore di EJB maggior parte del lavoro sporco, demandandolo al container. Parallelamente si evince in modo piuttosto lampante come sia importante la fase di analisi a priori e l’adozione di appropriate tecniche di design.
 

Vai alla Home Page di MokaByte
Vai alla prima pagina di questo mese


MokaByte®  è un marchio registrato da MokaByte s.r.l.
Java®, Jini®  e tutti i nomi derivati sono marchi registrati da Sun Microsystems; tutti i diritti riservati
E' vietata la riproduzione anche parziale
Per comunicazioni inviare una mail a
info@mokabyte.it