Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Gestione della concorrenza con Nhibernate

Gestire le modifiche contemporanee ai dati
Gestire le modifiche contemporanee ai dati
Link copiato negli appunti

Come noto, nei sistemi informatici si parla di concorrenza quando più utenti, contemporaneamente, effettuano l'accesso e modificano un database (o in generale una risorsa).

Supponiamo ad esempio di avere un'entità prodotto composta solamente dai campi codice, nome e prezzo; in uno scenario di concorrenza due utenti, Marco e Luigi, visualizzano lo stesso prodotto chiamato "HtmlCup" tramite una pagina Web. Marco effettua un cambiamento al nome prodotto modificandolo in "Html Cup", subito dopo Luigi cambia il prezzo del prodotto, ma quando le sue modifiche vengono inviate al database, la modifica di Marco viene sovrascritta, riportando il nome al valore "HtmlCup".

Questo problema si può facilmente codificare: si aprono due sessioni NHibernate e in ognuna di esse viene caricata la stessa entità, ogni sessione modifica una proprietà e le modifiche vengono propagate al database con il metodo Flush().

La prima modifica viene purtroppo sovrascritta dalla seconda, perché NHibernate, al momento di effettuare il secondo salvataggio, è inconsapevole del fatto che il valore originale è stato nel frattempo modificato. In un simile scenario dove la concorrenza viene ignorata si applica, spesso inconsapevolmente, la soluzione detta last update wins, ovvero: l'ultimo update vince e sovrascrive eventuali modifiche effettuate nel frattempo all'entità.

Listato1 dell'esempio allegato

private static void Listato1()
{ // sessione di Marco
  using (ISession session1 = NHSessionManager.GetSession()) 
  {
    Product1 prouctLoadedByMarco = session1.Get<Product1>(HtmlCupId);
    
    // sessione di Luigi
    using (ISession session2 = NHSessionManager.GetSession())
    {
      Product1 prouctLoadedByLuigi = session2.Get<Product1>(HtmlCupId);
      // Sono Luigi e inserisco uno spazio
      prouctLoadedByLuigi.Code = "Html Cup"; 
      session2.Flush(); // Le modifiche vengono propagate
    }
    
    prouctLoadedByMarco.Price *= (Decimal)0.9; // Applico uno sconto
    // Salvo, ma in questo caso ho sovrascritto le modifiche di Luigi
    session1.Flush(); 
  }
  // Le modifiche di Luigi non sono più presenti nel database
}

Ora che si conosce il problema è necessario capire come gestirlo, in primo luogo è possibile richiedere un update dinamico di una classe, ovvero generare istruzioni SQL di tipo UPDATE che vanno ad aggiornare solo i campi effettivamente modificati dal momento della lettura. Per far questo basta aggiungere la direttiva dynamic-update="true" nel mapping:

<class name="Product1" table="Products" lazy="false" 
       dynamic-update="true" >

Eseguendo nuovamente il Listato1() si può notare come le istruzioni di update riguardano ora i soli campi effettivamente modificati, permettendo cosi la modifica concorrente di proprietà differenti. Ecco come si presentano le istruzioni SQL:

NHibernate: UPDATE dbo.Products SET Code = @p0 WHERE Id = @p1; @p0 = 'Html Cup', @p1 = '1'
NHibernate: UPDATE dbo.Products SET Price = @p0 WHERE Id = @p1; @p0 = '20.304000', @p1 = '1'

Questa tecnica non è comunque considerata un'effettiva gestione della concorrenza, anche perché non risolve il problema della modifica contemporanea della stessa proprietà. Naturalmente NHibernate integra al suo interno varie metodologie di gestione di concorrenza che possono essere attivate se necessario.

NHibernate e concorrenza ottimistica

Il termine Concorrenza ottimistica deve il suo nome al principio su cui si basa la gestione dei conflitti. Si presuppone infatti "ottimisticamente", che i reali casi di concorrenza siano pochi ed è quindi sufficiente controllare, ad ogni aggiornamento, se l'entità è stato modificata dal momento della lettura.

In caso di conflitto il programmatore viene informato dell'anomalia con un'eccezione e deve quindi provvedere ad eseguire un apposita azione correttiva, che può essere un semplice avvertimento all'utente, una procedura automatica di risoluzione dei conflitti oppure può semplicemente abortire l'operazione ed iniziare da capo con i dati aggiornati.

Figura 1. Gestione di concorrenza di tipo ottimistico
Gestione di concorrenza di tipo ottimistico

Nel codice di esempio l'entità Product2 è stata mappata nella stessa tabella dell'entità precedente, ma nel mapping si richiede una gestione di concorrenza di tipo ottimistico grazie all'attributo optimistic-lock="all".

<class name="Product2" table="Products" lazy="false"
       optimistic-lock="all" dynamic-update="true">

Se osserviamo il Listato2(), troviamo lo stesso flusso di istruzioni del Listato1(), ma questa volta utilizziamo classe Product2 su cui è attivo il controllo di concorrenza.

Listato2()

private static void Listato2()
{
  using (ISession session1 = NHSessionManager.GetSession()) 
  {
    Product2 prouctLoadedByMarco = session1.Get<Product2>(HtmlCupId);
    
    using (ISession session2 = NHSessionManager.GetSession())
    {
      Product2 prouctLoadedByLuigi = session2.Get<Product2>(HtmlCupId);
      prouctLoadedByLuigi.Code = "Html Cup"; // Ho inserito uno spazio
      session2.Flush(); // Le modifiche vengono propagate
    }
    
    prouctLoadedByMarco.Price *= (Decimal)0.9; // Applico uno sconto
    
    // Salvo, ho un eccezione perchè ho rilevato la concorrenza
    session1.Flush(); 
  }
}

Se si osservano le effettive istruzioni di update lanciate da NHibernate, si può notare come ora siano decisamente più complesse rispetto al caso precedente.

NHibernate: UPDATE dbo.Products SET Code = @p0 WHERE Id = @p1 AND Code=@p2 AND Price=@p3; 
            @p0 = 'Html Cup', @p1 = '1', @p2 = 'HtmlCup', @p3 = '22.56000'

NHibernate: UPDATE dbo.Products SET Price = @p0 WHERE Id = @p1 AND Code=@p2 AND Price=@p3; 
            @p0 = '20.304000', @p1 = '1', @p2 = 'HtmlCup', @p3 = '22.56000'

Quando la concorrenza ottimistica è impostata su all, NHibernate effettua un update aggiungendo nella clausola where, non solo la condizione sull'id (che individua l'entità da aggiornare), ma anche l'uguaglianza di tutte le proprietà al loro valore originale. In questo modo se la riga del db è stata modificata in un momento successivo alla lettura, nessun dato verrà aggiornato perché le condizioni della where non sono soddisfatte!

L'oggetto ISession controlla poi il numero di righe effettivamente aggiornate e se tale valore è zero lancia una StaleObjectStateException, per indicare un avvenuto conflitto. In caso di StaleObjectStateException è poi compito del programmatore eseguire un'azione correttiva.

Se si desidera permettere modifiche contemporanee di proprietà distinte, è sufficiente impostare a dirty il valore della proprietà dynamic-update. In questo modo nella clausola where viene eseguito il controllo solo sulle proprietà effettivamente modificate. Si permette la modifica concorrente di proprietà distinte, ma se due utenti modificano contemporaneamente la stessa proprietà, verrà lanciata la StaleObjectStateException.

NHibernate e timestamp

L'uso di timestamp prevede un approccio alla gestione della concorrenza basato sul confronto tra il valore attuale e quello al momento della lettura. Ha il vantaggio di funzionare per qualsiasi entità, ma lo svantaggio di essere oneroso in termini di performance durante l'update. Dovendo infatti imporre una condizione su tutti o alcuni campi, difficilmente la query potrà usufruire degli indici del database e in caso di tabelle molto grandi le performance potrebbero risentirne.

Con un optimistic-lock di tipo all infatti nessun indice potrà mai essere coprente per una query di update.

Se non è necessario permettere la modifica concorrente di proprietà distinte, la soluzione migliore è usare una colonna timestamp. Nell'oggetto ConcurrentProduct viene impiegato questo approccio, aggiungendo semplicemente un campo di tipo System.DateTime chiamato timestamp e modificando il mapping aggiungendo un tag timestamp subito dopo il campo <id>

<timestamp name="timestamp" access="field" type="System.DateTime"/>

Nel Listato3() inseriamo questa nuova classe:

Listato3()

private static void Listato3()
{
  using (ISession session1 = NHSessionManager.GetSession())
  {
    ConcurrentProduct prouctLoadedByMarco = session1.Get(HtmlCupIdConcurrent);
    
    using (ISession session2 = NHSessionManager.GetSession())
    {
      ConcurrentProduct prouctLoadedByLuigi = session2.Get(HtmlCupIdConcurrent);
      prouctLoadedByLuigi.Code = "Html Cup"; // Ho inserito uno spazio
      session2.Flush(); // Le modifiche vengono propagate
    }
    
    prouctLoadedByMarco.Price *= (Decimal)0.9; // Applico uno sconto
    
    session1.Flush(); // Salvo, ho un eccezione perchè ho rilevato la concorrenza
  }
}

Lanciando questo metodo possiamo notare come le istruzioni di update siano decisamente più coincise.

NHibernate: 
UPDATE dbo.ConcurrentProduct SET timestamp = @p0, Code = @p1 
WHERE Id = @p2 AND timestamp = @p3; 
@p0 = '12/16/2008 2:24:17 PM', @p1 = 'Html Cup', @p2 = '1', @p3 = '12/16/2008 2:24:14 PM'

NHibernate: 
UPDATE dbo.ConcurrentProduct SET timestamp = @p0, Price = @p1 
WHERE Id = @p2 AND timestamp = @p3; 
@p0 = '12/16/2008 2:24:17 PM', @p1 = '20.304000', @p2 = '1', @p3 = '12/16/2008 2:24:14 PM'

Non importa che l'oggetto abbia una, due o dieci proprietà, nella clausola where di update utilizziamo come condizione l'uguaglianza dell'id e del timestamp originale. In questo caso il campo timestamp non assume nessun significato a livello di business ed è legato al concetto di persistenza.

Questo è un classico esempio di come gli oggetti non possano essere sempre persistent ignorant ovvero, talvolta è necessario tenere in considerazione l'orm utilizzato durante il design delle classi.

Purtroppo l'utilizzo di una data non salva da update effettuati nello stesso istante di tempo, evento raro ma comunque possibile; inoltre il campo data occupa ben 8 byte, aumentando la dimensione degli indici del database. Una soluzione alternativa è utilizzare un numero di versione di tipo System.Int32 che può essere incrementato ad ogni modifica. Per fare questo è sufficiente cambiare il tipo di dato del campo timestamp ad Int32, modificando contestualmente il mapping nel modo seguente:

<version name="timestamp" access="field" type="System.Int32"/> 

Il tag <version> indica a NHibernate di considerare quel campo come la "versione" dell'oggetto, ad ogni update la versione viene incrementata di uno ed usata per verificare l'avvenuta concorrenza. Il vantaggio è che rispetto al timetsamp di tipo DateTime, con la versione non si corre il rischio di avere due modifiche contemporanee, ed il campo è lungo solamente 4 byte rispetto agli 8 di un campo DateTime.

Concorrenza ed oggetti disconnessi

Quando un oggetto viene disconnesso, sia perche la sessione viene chiusa, o perché lo si è rimosso dalla sessione manualmente tramite session.evict(), NHibernate non possiede più il suo contesto di persistenza e questa differenza influisce in modo drastico sulla gestione della concorrenza ottimistica.

Nel Listato4() abbiamo lo stesso scenario del Listato2: concorrenza con una classe mappata con optimistic-lock="all", con l'unica differenza che questa volta l'oggetto viene disconnesso, modificato ed infine riconnesso con l'istruzione Session.Update().

Listato4()

private static void Listato4()
{
  Product2 prouctLoadedByMarco;
  
  using (ISession session1 = NHSessionManager.GetSession())
  {
    prouctLoadedByMarco = session1.Get<Product2>(HtmlCupId);
  } // L'oggetto viene disconnesso
  
  using (ISession session2 = NHSessionManager.GetSession())
  {
    Product2 prouctLoadedByLuigi = session2.Get<Product2>(HtmlCupId);
    prouctLoadedByLuigi.Code = "Html Cup"; // Ho inserito uno spazio
    session2.Flush(); // Le modifiche vengono propagate
  }
  
  prouctLoadedByMarco.Price *= (Decimal)0.9; // Applico uno sconto
  
  using (ISession session3 = NHSessionManager.GetSession())
  {
    session3.Update(prouctLoadedByMarco);
    // Salvo, ma in questo caso ho sovrascritto le modifiche di Luigi
    session3.Flush(); 
  }
  
  // Le modifiche di Luigi nonsono più presenti nel database
}

In questo caso non viene scatenata nessuna eccezione, ed inoltre la modifica di Luigi viene sovrascritta, proprio come se la concorrenza non fosse stata impostata. La ragione di ciò è semplice, quando si chiama il metodo session.Update(), NHibernate non ha modo di conoscere il valore delle proprietà al momento in cui l'oggetto era stato letto, dato che è in stato disconnesso e quindi non ha altra soluzione che effettuare un update su tutti i campi, ignorando ogni forma di concorrenza.

Il Listato5() evidenzia invece come la scelta di utilizzare un <version> o un <timestamp>, permette di gestire in maniera corretta la concorrenza anche in caso di oggetto disconnesso.

Listato5()

private static void Listato5()
{
  ConcurrentProduct prouctLoadedByMarco;
  using (ISession session1 = NHSessionManager.GetSession())
  {
    prouctLoadedByMarco = session1.Get(HtmlCupIdConcurrent);
  }
  
  using (ISession session2 = NHSessionManager.GetSession())
  {
    ConcurrentProduct prouctLoadedByLuigi = session2.Get(HtmlCupIdConcurrent);
    prouctLoadedByLuigi.Code = "Html Cup"; // Ho inserito uno spazio
    session2.Flush(); // Le modifiche vengono propagate
  }

  prouctLoadedByMarco.Price *= (Decimal)0.9; //Applico uno sconto

  using (ISession session3 = NHSessionManager.GetSession())
  {
    session3.Update(prouctLoadedByMarco);
    // Salvo, ho eccezione anche in oggetti disconnessi 
    // il timestamp mi permette di rilevare la concorrenza
    session3.Flush();
  }
} 

Dato che l'effettivo valore di versione o timestamp è contenuto nell'oggetto in una proprietà, rimane inalterato anche in caso di disconnessione e quindi può essere sempre utilizzato durante l'update. Se si può influire sul design del database, oppure se si lascia generare il database a nhibernate, l'uso di version o timestamp è quindi nettamente superiore all'uso di un optimistic-lock="all" o "Dirty". Si ricordi comunque che un lock ottimistico di tipo dirty permette la modifica concorrente di proprietà differenti, opzione non disponibile con version o timestamp.

Lock pessimistici

Come detto precedentemente, il lock ottimistico viene usato quando si presuppone che i conflitti siano rari e che in caso di concorrenza sia possibile adottare una azione di compensazione adeguata. Se questa premessa non è vera ed i conflitti non possono essere gestiti ragionevolmente in nessun modo, si può adottare una gestione della concorrenza di tipo pessimistico.

Il nome pessimistico deriva dal fatto che l'eventualità di un conflitto è veramente grave e deve essere evitata a tutti i costi.

Esistono differenti soluzioni a questo problema, ma tutte fanno uso di lock; in pratica quando un oggetto viene caricato da un utente viene bloccato e nessun altro può modificarlo fino a che il primo non ha rilasciato il lock. Nhibernate supporta questa tecnica in modo nativo, nel Listato6() viene infatti mostrato come imporre un lock pessimistico ad un oggetto

private static void Listato6()
{
  using (ISession session1 = NHSessionManager.GetSession())
  {
    session1.BeginTransaction();
    Product1 prouctLoadedByMarco = 
             session1.Get<Product1>(HtmlCupId, LockMode.UpgradeNoWait);
    
    using (ISession session2 = NHSessionManager.GetSession())
    {
      // Sono bloccato qui
      Product1 prouctLoadedByLuigi = session2.Get<Product1>(HtmlCupId, LockMode.UpgradeNoWait); 
      prouctLoadedByLuigi.Code = "Html Cup";
      session2.Flush();
    }
    
    prouctLoadedByMarco.Price *= (Decimal)0.9; 
    session1.Flush();
  }
}

La prima condizione è avere iniziato una transazione, con questa precondizione, per imporre un lock pessimistico, l'oggetto viene recuperato con un lockmode di tipo upgrade. Questo significa che la session1 sa che l'oggetto verrà modificato e quindi nessun altra sessione potrà leggere l'oggetto con un lockmode di tipo upgrade fino a che la transazione non sarà completa.

Questa tecnica diminuisce drasticamente la scalabilità, Luigi in questo caso blocca l'oggetto e Marco non può accedervi fino a che il lock non termina. Viene da se che la concorrenza pessimistica può essere utilizzata solamente se i lock hanno una durata temporale ridotta.

Ad esempio l'applicazione di lock è una soluzione impraticabile in scenari Web, data la natura asincrona dell'interazione. Applicare un lock tra due azioni (postback o chiamate ajax) dell'utente è impensabile, perché non si è in grado di stimare l'intervallo di tempo che intercorrerà tra di esse. In casi limite l'utente potrebbe anche caricare un oggetto per modificarlo ed andarsene non completando mai la transazione.

Un'applicazione possibile di concorrenza pessimistica si può invece immaginare per operazioni di update batch, oppure in scenari in cui non è presente nessuna interazione con l'utente e quindi il tempo di lock può essere stimato in maniera ragionevole.

Transparent Write Behind

Nel Listato8() viene evidenziato un possibile problema che si manifesta con la gestione di concorrenza di tipo ottimistico. In questo scenario si focalizza l'attenzione sul concetto di transparent-write-behind ovvero la capacità di NHibernate di persistere in maniera trasparente tutte le modifiche fatte ad oggetti che sono in stato persistente.

Una regola fondamentale della versione 2.0 è che tutte le comunicazioni con il database debbono essere incluse in transazione, in caso contrario non viene effettuato l'autoFlush che modifica in maniera automatica gli oggetti modificati. Questo viene fatto per evitare che le modifiche vengano eseguite in maniera non transazionale.

Listato8()

private static void Listato8()
{
  using (ISession session1 = NHSessionManager.GetSession())
  {
    session1.BeginTransaction();
    Product2 prouctLoadedByMarco = session1.Get(HtmlCupId);
    
    using (ISession session2 = NHSessionManager.GetSession())
    {
      Product2 prouctLoadedByLuigi = session2.Get(HtmlCupId);
      prouctLoadedByLuigi.Code = "Html Cup"; // Ho inserito uno spazio
      session2.Flush(); // Le modifiche vengono propagate
    }
    
    prouctLoadedByMarco.Price *= (Decimal)0.9; // Applico uno sconto
    
    IList products = session1.CreateQuery("select P from Product2 P where P.Price > 20").List();
    session1.Transaction.Commit();
  }
}

Lanciando il Listato8(), viene sollevata l'eccezione di StaleObjectStateException viene rilasciata nell'istruzione:

IList<Product2> products = session1.CreateQuery("select P from Product2 P where P.Price > 20").List<Product2>();

Vediamo cosa succede: la sessione è in transazione, Nhibernate rileva che sta per essere effettuata una query sui prodotti, quindi esegue un Flush() automatico per propagare eventuali modifiche al database e per impedire che che la query ritorni dati errati.

Questo esempio dimostra che le eccezioni di concorrenza ottimistica possono essere lanciate anche prima dell'effettiva chiamata al metodo Flush().

Una possibile soluzione è impostare il FlushMode della sessione al valore Never, in questo modo le modifiche vengono propagate solo ed esclusivamente quando l'utente chiama esplicitamente il metodo Flush(). Se utilizziamo questa modalità, dobbiamo prepararci a possibili incongruenze, come mostrato nel Listato9(), in cui si carica un prodotto, si modifica il prezzo applicando uno sconto e poi si effettua una query per recuperare tutti i prodotti il cui prezzo è maggiore di 20.

private static void Listato9()
{
  using (ISession session1 = NHSessionManager.GetSession())
  {
    session1.FlushMode = FlushMode.Never;
    session1.BeginTransaction();
    Product2 prouctLoadedByMarco = session1.Get<Product2>(HtmlCupId);
    using (ISession session2 = NHSessionManager.GetSession())
    {
      Product2 prouctLoadedByLuigi = session2.Get<Product2>(HtmlCupId);
      prouctLoadedByLuigi.Code = "Html Cup"; // Ho inserito uno spazio
      session2.Flush(); // Le modifiche vengono propagate
    }
    
    prouctLoadedByMarco.Price *= (Decimal)0.9; // Applico uno sconto
    
    Console.WriteLine("Stampa prodotti il cui prezzo è maggiore di 20");
    
    IList<Product2> products = session1.CreateQuery("select P from Product2 P where P.Price > 20").List<Product2>();
    foreach (Product2 product in products)
    {
      Console.WriteLine("Code {0} price: {1}", product.Code, product.Price);
    }
    
    session1.Transaction.Commit(); 
  }
}

In questo caso purtroppo, dato che il FlushMode è a Never, NHibernate effettua la query senza avere propagato le modifiche al database. Questa situazione è anomala, perché l'oggetto in memoria ha uno stato che è differente da quello del database, la query restituisce quindi il prodotto HtmlCup sebbene il suo prezzo sia stato modificato e non sia più maggiore di 20. Il risultato è che il programma scrive:

Stampa prodotti il cui prezzo è maggiore di 20

NHibernate: SELECT product2x0_.Id as Id0_, product2x0_.Code as Code0_, product2x0_.Price as Price0_ from dbo.Products product2x0_ WHERE (product2x0_.Price>20 )

Code HtmlCup price: 18.891000 

L'anomalia in questo caso è che nonostante io abbia richiesto i prodotti il cui costo è maggiore di 20 viene stampato HtmlCup il cui prezzo è 18.89.

Conclusioni

I problemi di concorrenza sono solitamente complessi e sebbene NHibernate metta a disposizione alcune tecniche per la gestione ottimistica/pessimistica, il grosso del lavoro è comunque lasciato al programmatore. In situazioni dove i problemi di concorrenza assumono un'importanza fondamentale è bene capire fin dalle prime fasi del progetto come strutturare l'applicativo, in modo da creare una infrastruttura che permetta una gestione adeguata.

Ad esempio si potrebbe prevedere per tutte le interfacce una possibile modalità di confronto dove, in caso di avvenuta eccezione di concorrenza, a sinistra viene mostrata l'entità con le nuove modifiche e a destra l'entità con le modifiche dell'utente. In questo caso è l'utente che procederà poi ad effettuare un merge.

Riferimenti

  • [BAUER-KING]: Java Persistence With Hibernate

Ti consigliamo anche