Esempi di errori nelle transazioni concorrenti


1. Overview

Vogliamo che il frontend del nostro shop controlli che i prodotti richiesti da un cliente siano disponibili. In tal caso viene visualizzata l'eventuale fattura al cliente, il quale nel mondo reale avra' la possibilita' di accettarla e proseguire l'acquisto o rifiutarla. Nel nostro esempio "scolastico" dopo aver visualizzato la fattura al cliente verranno attesi 10 secondi dal negozio prima di procedere con l'acquisto. L'acquisto termina con un resoconto fornito al cliente.

Viene fornito quindi un Client "acquirente" e un servizio per l'accettazione di acquisti. Verra fornito anche un client per il popolamento del magazzino ed il servizio corrispondente lato server. Tale client funge da fornitore di articoli per il negozio.

Il pacchetto con l'ambiente dell'esercitazione e' scaricabile da http://www.link.it/isi/doc/Esercitazione/1.4.2/esercitazione_db.zip

L'esercitazione inizia con un'analisi dei servizi forniti:

  • Creazione database utilizzato per il negozio di shop

  • Analisi del servizio di magazzino esposto dal negozio di shop

  • Analisi del servizio di vendita esposto dal negozio di shop

  • Deploy del negozio di shop

  • Analisi/Utilizzo del client fornitore che popola il magazzino

  • Analisi/Utilizzo del client acquirente che permette di comprare articoli

L'esercitazione prosegue facendo vedere come sia possibile far avvenire i problemi sulle transazioni concorrenti viste a lezione:

  • Lost Update

  • Unreapeatable Read

  • Phantom Row

L'esercitazione, finora solo di analisi, diventa operativa. Dovra' essere compreso, per ogni problema descritto nel precedente capitolo, quale soluzione descritta a lezione e' possibile adottare.

2. Analisi servizi forniti

2.1. Creazione database utilizzato per il negozio di shop

Di seguito il comando SQL per creare la tabella che conterra' gli articoli se non e' ancora stato fatto. Eseguire la creazione della tabella dopo essersi collegati al database tramite il comando psql -h squalo -D login

CREATE TABLE listino(
    	codArticolo VARCHAR(255) NOT NULL,
    	descrizione VARCHAR(255) NOT NULL,
    	disponibilita INT NOT NULL,
    	prezzo FLOAT NOT NULL
    );

I servizi che verranno illustrati di seguito devono essere configurati per poter accedere al database. La configurazione e' cablata nella classe isi.servlet.CostantiDatabase. Modificate le seguenti costanti:

  • URL_DATABASE, il nome della macchina dove e' disponibile il database (per l'esercitazione utilizzare squalo)

  • NOME_DATABASE, il nome del database (per l'esercitazione utilizzare il vostro login)

  • USERNAME_ACCESSO_DATABASE, il nome dell'utente per collegarsi al database (per l'esercitazione utilizzare il vostro login)

  • PASSWORD_ACCESSO_DATABASE, la password per collegarsi al database (per l'esercitazione utilizzare la stringa vuota "")

2.2. Analisi del servizio di magazzino esposto dal negozio di shop

La classe isi.servlet.Magazzino, espone un servizio che permette ai fornitori di inserire articoli nel magazzino. Questa classe riceve messaggi xml definiti dallo schema XSD shop/WEB-INF/magazzino.xsd definito come segue:

<?xml version="1.0" encoding="UTF-8"?><xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
    xmlns:ns="http://www.shop.com/ns" 
    targetNamespace="http://www.shop.com/ns"
    elementFormDefault="qualified">
    
        <xsd:element name="magazzino" type="ns:magazzinoType"/>
    
        <xsd:complexType name="magazzinoType">
            <xsd:sequence>
                <xsd:element name="articolo" type="ns:articoloType" maxOccurs="unbounded"/>
                <xsd:element name="distributore" type="xsd:string"/>
            </xsd:sequence>
        </xsd:complexType>
    
        <xsd:complexType name="articoloType">
            <xsd:sequence>
                <xsd:element name="nome" type="ns:nomeType"/>
                <xsd:element name="descrizione" type="xsd:string"/>
                <xsd:element name="quantita" type="xsd:int"/>
                <xsd:element name="prezzo" type="xsd:float"/>
            </xsd:sequence>
        </xsd:complexType>
    
        <xsd:simpleType name="nomeType">
            <xsd:restriction base="xsd:string">
                <xsd:pattern value="[A-Z0-9]{7}"/>
            </xsd:restriction>
        </xsd:simpleType>
    
    </xsd:schema>

Per ogni articolo viene fornito il codiceArticolo (elemento nome), una descrizione, la quantita' di articoli da inserire e il prezzo del singolo articolo. Inoltre il fornitore inserisce la propria identita' nell'elemento distributore.

Il servizio e' una servlet. Il metodo doGet richiama il metodo privato della classe modificaMagazzino che si occupa effettivamente di inserire gli articoli forniti. Tale metodo effettua il parsing del messaggi raccogliendo l'identita del fornitore (leggendo l'elemento distributore) e i vari articoli. Per ogni articolo effettua l'inserimento in magazzino.

Il metodo modificaMagazzino si occupa effettivamente della modifica alla base di dati effettuando la connessione al database ed effettuando l'inserimento dell'articolo se non non era mai stato catalogato prima, o modificandone la quantita' di elementi presenti. Analizziamo nel dettaglio il codice che si occupa della gestione del database:

  • Gestire l'accesso al database, e ottentere una connessione con auto-commit a true. Ogni operazione che verra effettuata' sul database viene immediatamente apportata sulla base di dati.

        // Carica la classe del driver JDBC
        Class.forName(CostantiDatabase.DRIVER_JDBC);
        
        // Ottengo connessione al database
        connectionDB = DriverManager.getConnection( 
            CostantiDatabase.CONNECTION_URL,
            CostantiDatabase.USERNAME_ACCESSO_DATABASE,
            CostantiDatabase.PASSWORD_ACCESSO_DATABASE );
        if(connectionDB==null){
            log.error("Connection is null");
        }
        
        // Ogni operazione viene immediatamente riportata sulla base di dati
        connectionDB.setAutoCommit(true);
                        
  • Viene effettuata una ricerca nel magazzino per verificare se l'articolo che il fornitore desidera inserire e' gia stato catalogato in precedenza. Se l'articolo risulta gia' presente (rs.next()==true) attraverso la query e' possibile ottenere la disponibilita' dell'articolo.

        // Effettuo ricerca
        String sqlQuery = "SELECT "+CostantiDatabase.LISTINO_DISPONIBILITA+" FROM "+CostantiDatabase.LISTINO +" WHERE "+CostantiDatabase.LISTINO_CODICE_ARTICOLO+"=?";
        pstmt = connectionDB.prepareStatement(sqlQuery);
        pstmt.setString(1, nome);
        rs = pstmt.executeQuery();
        boolean articoloPresenteInMagazzino = rs.next();
        rs.close();
        pstmt.close();
                        
  • Attraverso la precedente query, se ci accorgiamo che l'articolo non e' presente in magazzino significa che tale articolo non e' mai stato catalogato. In questo caso dobbiamo effettuare una INSERT.

        if(articoloPresenteInMagazzino==false){
            // Insert
            String insertQuery = "INSERT INTO "+CostantiDatabase.LISTINO+" VALUES (?,?,?,?)";
            pstmtAcquisizione = connectionDB.prepareStatement(insertQuery);
            pstmtAcquisizione.setString(1, nome);
            pstmtAcquisizione.setString(2, descrizione);
            pstmtAcquisizione.setInt(3, quantita);
            pstmtAcquisizione.setFloat(4, prezzo);
            int entriesModificate = pstmtAcquisizione.executeUpdate();
            if(entriesModificate<0){
                log.error(idCliente+" Aggiornamento disponibilita "+nome+", non riuscita, articolo non inserito");
                return false;
            }				
                log.info(idCliente+" "+quantita+" articoli ["+nome+"] sono stati aggiunti in magazzino con successo.");
            }
        }
  • Attraverso la precedente query, se ci accorgiamo che l'articolo risulta gia' presente dobbiamo solo aggiornarne la disponibilita'. In questo caso dobbiamo effettuare un'UPDATE.

        else{
            // Update
            String updateQuery = "UPDATE "+CostantiDatabase.LISTINO+" set "+CostantiDatabase.LISTINO_DISPONIBILITA+"="+CostantiDatabase.LISTINO_DISPONIBILITA+"+? WHERE "+CostantiDatabase.LISTINO_CODICE_ARTICOLO+"=?";
            pstmtAcquisizione = connectionDB.prepareStatement(updateQuery);
            pstmtAcquisizione.setInt(1, (quantita));
            pstmtAcquisizione.setString(2, nome);
            int entriesModificate = pstmtAcquisizione.executeUpdate();
            if(entriesModificate<0){
                log.error(idCliente+" Aggiornamento disponibilita "+nome+", non riuscita, articolo non presente!");
                return false;
            }				
            log.info(idCliente+" "+quantita+" articoli ["+nome+"] sono stati aggiunti in magazzino con successo.");
        }

2.3. Analisi del servizio di vendita esposto dal negozio di shop

La classe isi.servlet.Acquisti, espone un servizio che permette ai clienti di acquistare articoli presenti nel magazzino. Questa classe riceve messaggi xml definiti dallo schema XSD shop/WEB-INF/ordine.xsd definito come segue:

<?xml version="1.0" encoding="UTF-8"?><xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
xmlns:ns="http://www.shop.com/ns" 
targetNamespace="http://www.shop.com/ns"
elementFormDefault="qualified">

    <xsd:element name="ordine" type="ns:ordineType"/>

    <xsd:complexType name="ordineType">
        <xsd:sequence>
            <xsd:element name="articolo" type="ns:articoloType" maxOccurs="unbounded" minOccurs="0"/>
        </xsd:sequence>
        <xsd:attribute name="client" type="xsd:string" use="required"/>
    </xsd:complexType>

    <xsd:complexType name="articoloType">
        <xsd:sequence>
            <xsd:element name="nome" type="ns:nomeType"/>
            <xsd:element name="quantita" type="xsd:positiveInteger"/>
        </xsd:sequence>
    </xsd:complexType>

    <xsd:simpleType name="nomeType">
        <xsd:restriction base="xsd:string">
            <xsd:pattern value="[A-Z0-9]{7}"/>
        </xsd:restriction>
    </xsd:simpleType>

</xsd:schema>

L'ordine e' composto da un attributo client che identifica il cliente. L'ordine puo' poi contenere una serie di elementi articolo che definiscono il codice dell'articolo e la quantita' desiderata. Si puo' notare come la presenza di un elemento articolo e' opzionale poiche' il servizio prevede la possibilita' che il cliente desideri acquistare tutti gli articoli presenti nel magazzino. Questo caso viene adottato se l'ordine non contiene articoli.

Il servizio e' una servlet. Il metodo doGet richiama i metodi di gestione del carrello. Il metodo updateCarrello e' quello che si occupa effettivamente di effettuare l'acquisto degli articoli richiesti nell'ordine. Tale metodo effettua il parsing del messaggi raccogliendo l'identita del cliente (leggendo l'attributo client) e i vari articoli.

    Document doc = validazioneOrdine(req);
		
    // Identita Cliente
    String idCliente = "["+doc.getFirstChild().getAttributes().getNamedItem("client").getNodeValue()+"]";
    
    //Cerco gli articoli nell'ordine e li aggiungo al carrello.
    NodeList articoli = doc.getElementsByTagName("ns:articolo");
    Vector<Articolo> articoliDaComprare = new Vector<Articolo>();
    if(articoli!=null && articoli.getLength()>0){
    for(int i = 0; i < articoli.getLength(); i++){
    		Element articoloElement = (Element) articoli.item(i);
    		Articolo articolo = new Articolo(articoloElement.getFirstChild().getTextContent(), Integer.parseInt(articoloElement.getLastChild().getTextContent()));
    		log.info(idCliente+" Richiesta acquisto " + articolo.nome + ": pezzi n. " + articolo.quantita);
    		articoliDaComprare.add(articolo);
    	}
    }
    
    acquistaArticolo(articoliDaComprare,idCliente,carrello);
            

Il metodo acquistaArticolo si occupa di accedere al database e di procedere con l'acquisto degli articoli desiderati dal client. Il metodo si occupa di:

  • Gestire l'accesso al database, e ottentere una connessione con auto-commit a false. Ogni operazione che verra effettuata sul database non sara' effettivamente apportata sulla base di dati fino a che non verra' eseguito il commit.

    // Carica la classe del driver JDBC
    Class.forName(CostantiDatabase.DRIVER_JDBC);
       
    // Ottengo connessione al database
    connectionDB = DriverManager.getConnection( 
        CostantiDatabase.CONNECTION_URL,
        CostantiDatabase.USERNAME_ACCESSO_DATABASE,
        CostantiDatabase.PASSWORD_ACCESSO_DATABASE );
    if(connectionDB==null){
        log.error("Connection is null");
    }
       
    // gestisco la transazione applicativamente
    connectionDB.setAutoCommit(false);
                            
  • Reperire e visualizzare gli articoli attualmente presenti nel magazzino. Viene effettuata una query SQL sul database; tutti gli articoli trovati con relative disponibilita' e prezzi vengono ritornati dal metodo.

    // Visualizzo listino degli articoli disponibili
    Vector<Articolo> articoliDisponibili = readArticoliDisponibiliInMagazzino(idCliente, connectionDB);
                            
  • Verificare la disponibilita' degli articoli richiesti dal client. Se tutti gli articoli richiesti sono presenti viene stampata la fattura che il cliente dovra' pagare.

    // Check disponibilita
    // Se gli articoli da comprare non sono stati specificati significa che il cliente desidera comprare tutti gli articoli disponibili
    float fatturaArticoliRichiesti = fatturaArticoliRichiesti(idCliente, articoliDaComprare, articoliDisponibili);
    if(fatturaArticoliRichiesti==-1)
       return false;
                            
  • Simulazione del tempo di accettazione della fattura da parte del cliente.

    log.info(idCliente+" Simulazione interazione con il client per richiedere se desidera procedere con l'acquisto....");
    simulazioneCalcoliInterni();
                            
  • Modifica della base di dati per procedere con l'acquisto degli articoli desiderati dal cliente. Viene effettuata la query SQL gia' precedentemente utilizzata per visualizzare gli articoli presenti nel magazzino, e vengono effettuate delle update SQL decrementando la disponibilita' degli articoli interessati al cliente.

    // Si Procede con l'acquisto degli articoli indicati in fattura
    acquisizioneArticoli(idCliente, connectionDB, articoliDaComprare, carrello);
                            
  • Se le precedenti operazioni sono andate a buon fine viene effettuato il commit, altrimenti il rollback.

    try{
    
        ... precedenti operazioni ....
    
        // Commit delle operazioni effettuate sul database	
        log.info(idCliente+" Commit, salvo modifiche al database");
        connectionDB.commit();
    
    }
    catch(Exception e){
        log.error(idCliente+" Errore generale: "+e.getMessage(),e);
    
        // rollback
        try{
            log.info(idCliente+" Rollback, annullo modifiche al database");
            connectionDB.rollback();
        }catch(Exception eRollback){}
    
    }
                            

Di seguito viene riportato il codice del metodo readArticoliDisponibiliInMagazzino che si occupa di reperire e visualizzare gli articoli attualmente presenti nel magazzino. Si tratta di una semplice query SQL che visualizza nel log e raccoglie tutti i valori di ogni articolo presente in magazzino in un oggetto isi.servlet.Articolo. Tutti gli articoli raccolti vengono inseriti in un java.util.Vector e ritornati dal metodo:

PreparedStatement pstmt = null;
ResultSet rs = null;
Vector<Articolo> articoli = new Vector<Articolo>();
try{
      
    log.info(idCliente+" ------------- Listino (Articoli) -------------");
    String sqlQuery = "SELECT * FROM "+CostantiDatabase.LISTINO;
    pstmt = connectionDB.prepareStatement(sqlQuery);
    rs = pstmt.executeQuery();
    log.info(idCliente+" ["+CostantiDatabase.LISTINO_CODICE_ARTICOLO+" - "+CostantiDatabase.LISTINO_DESCRIZIONE+" - "+CostantiDatabase.LISTINO_DISPONIBILITA+" - "+CostantiDatabase.LISTINO_PREZZO+"]");
    while(rs.next()){
        String codiceArticolo = rs.getString(CostantiDatabase.LISTINO_CODICE_ARTICOLO);
        int quantitaDisponibile = rs.getInt(CostantiDatabase.LISTINO_DISPONIBILITA);
        float prezzo = rs.getFloat(CostantiDatabase.LISTINO_PREZZO);
        log.info(idCliente+" ["+codiceArticolo+" - "
                    +rs.getString(CostantiDatabase.LISTINO_DESCRIZIONE)
                    +" - Num. "+quantitaDisponibile
                    +" - €. "+prezzo+" ]");
        if(quantitaDisponibile>0){
            Articolo articolo = new Articolo(codiceArticolo,quantitaDisponibile,prezzo);
            articoli.add(articolo);
        }
    }	
    rs.close();
    pstmt.close();
    log.info(idCliente+" --------------------- Fine Listino ----------------------------\n");
    
}finally{
    try{
        if( rs!=null )
            rs.close();
        if( pstmt!=null )
            pstmt.close();		
    }catch(Exception eClose){
        log.error(idCliente+" Rilascio risorse non riuscito: "+eClose.getMessage());
    }
}
return articoli;
                

Vediamo inoltre il codice del metodo acquisizioneArticoli che si occupa di eliminare dal magazzino gli articoli che il cliente ha accettato di comprare in seguito alla visualizzazione della fattura. Da notare come il metodo effettua nuovamente la stessa query SQL utilizzata dal precedente metodo readArticoliDisponibiliInMagazzino per raccogliere gli articoli che il cliente desidera acquistare. Ogni singola riga del resultSet della query viene utilizzata poi per produrre l'opportuno comando di UPDATE in modo da decrementare la disponibilita' dell'articolo in base alla richiesta del cliente. Una implementazione diversa avrebbe potuto non effettuare nuovamente la query SQL ma utilizzare la struttura dati ritornata dal precedente metodo readArticoliDisponibiliInMagazzino. Tale implementazione, sicuramente meno efficente, e' stata scelta poiche' permette di far avvenire i problemi sulle transazioni descritte a lezione (LostUpdate, UnrepeatableRead e PhantomRow) come vedremo nel resto dell'esercitazione.

  • La query viene costruita inserendo nelle condizioni di WHERE i vari codici degli articoli che il cliente desidera acquistare. Le condizioni sono messe in una relazione di OR tra loro. Infine esiste il caso speciale in cui il cliente desidera acquistare tutti gli articoli presenti nel magazzino. In questo caso non vengono aggiunte condizioni di WHERE.

    String sqlQuery = "SELECT * FROM "+CostantiDatabase.LISTINO;
    if(articoliDaComprare.size()>0){
        sqlQuery +=" WHERE ";
        for(int i=0; i<articoliDaComprare.size(); i++){
            if(i>0)
                sqlQuery +=" OR ";
            sqlQuery +=CostantiDatabase.LISTINO_CODICE_ARTICOLO+"=?";
        }
    }
    pstmt = connectionDB.prepareStatement(sqlQuery);
    if(articoliDaComprare.size()>0){
        sqlQuery +=" WHERE ";
        for(int i=0; i<articoliDaComprare.size(); i++){
            pstmt.setString((i+1), articoliDaComprare.get(i).nome);
        }
    }
  • La query costruita viene eseguita per raccogliere tutti i dati di ogni articolo presente nel magazzino.

    rs = pstmt.executeQuery();
    float totaleAcquisto = 0;
    log.info(idCliente+" ["+CostantiDatabase.LISTINO_CODICE_ARTICOLO+" - "+CostantiDatabase.LISTINO_DESCRIZIONE+" - "+CostantiDatabase.LISTINO_DISPONIBILITA+" - "+CostantiDatabase.LISTINO_PREZZO+"]");
    while(rs.next()){
        String codiceArticolo = rs.getString(CostantiDatabase.LISTINO_CODICE_ARTICOLO);
        int quantitaDisponibile = rs.getInt(CostantiDatabase.LISTINO_DISPONIBILITA);
        float prezzo = rs.getFloat(CostantiDatabase.LISTINO_PREZZO);
        Articolo articolo = new Articolo(codiceArticolo,quantitaDisponibile,prezzo);
                    
        .... Logica di acquisto .....
                    
    }
    rs.close();
    pstmt.close();
    log.info(idCliente+" Totale fattura E."+totaleAcquisto);
  • Infine viene evidenziata il codice che implementa la logica di acquisto. Se il cliente desidera acquistare tutti gli articoli in magazzino (ramo else) effettua un UPDATE impostando la quantita rimasta a 0. Se il cliente aveva richiesto solo determinati articoli, viene visto se l'articolo letto dal magazzino e' uno tra quelli interessati dal cliente. In tal caso viene decrementata la disponibilita' dell'articolo in base alla richiesta del cliente.

    // Aggiornamento quantita
    String updateQuery = "UPDATE "+CostantiDatabase.LISTINO+" set "+CostantiDatabase.LISTINO_DISPONIBILITA+"=? WHERE "+CostantiDatabase.LISTINO_CODICE_ARTICOLO+"=?";
    pstmtAcquisizione = connectionDB.prepareStatement(updateQuery);
    if(articoliDaComprare.size()>0){
        for(int i=0; i<articoliDaComprare.size(); i++){
            if(articoliDaComprare.get(i).nome.equals(codiceArticolo)){
                pstmtAcquisizione.setInt(1, (quantitaDisponibile-articoliDaComprare.get(i).quantita));	
                log.info(idCliente+" Num."+articoliDaComprare.get(i).quantita+" di "+codiceArticolo+" per un tot di E."+(articoliDaComprare.get(i).quantita*prezzo));
                totaleAcquisto += (articoliDaComprare.get(i).quantita*prezzo);
                articolo.quantita=articoliDaComprare.get(i).quantita;
                carrello.add(articolo);
                break;
            }
        }
    }
    else{
        pstmtAcquisizione.setInt(1, 0); // compro tutti
        log.info(idCliente+" Num."+quantitaDisponibile+" di "+codiceArticolo+" per un tot di E."+(quantitaDisponibile*prezzo));
        totaleAcquisto += (quantitaDisponibile*prezzo);
        carrello.add(articolo);
    }
    
    pstmtAcquisizione.setString(2, codiceArticolo);
    int entriesModificate = pstmtAcquisizione.executeUpdate();
    if(entriesModificate<0){
        log.error(idCliente+" Aggiornamento disponibilita "+codiceArticolo+", non riuscita, articolo non presente!");
        log.info(idCliente+" --------------------- Fine Resoconto Acquisto ----------------------------\n");
        return false;
    }

2.4. Deploy del negozio di shop

Per facilitare sviluppo e utilizzo dei componenti di questa esercitazione viene distribuito il build.xml per l'utilizzo del tool ant. Di questo strumento non viene richiesta la comprensione.

Editare il file local_env.xml e verificare che il parametro deploy_dir sia corretto.

Il servizio di shop e' compilabile tramite il comando ant make_war.shop.

Il servizio di shop e' installabile su tomcat tramite il comando ant deploy.shop.

I due servizi descritti nei precedenti paragrafi saranno disponibili alle seguenti url:

  • http://localhost:8080/shop/magazzino, servizio di magazzino

  • http://localhost:8080/shop/acquisti, servizio di vendita

2.5. Analisi/Utilizzo del client fornitore che popola il magazzino

Il client http che permette di popolare il magazzino con articoli viene implementato dalla classe isi.http.DistributoreClientHTTP. Richiede come input cinque argomenti, quattro dei quali definiscono l'articolo che si desidera aggiungere al magazzino, rispettivamente il codice articolo, la descrizione, la quantita' che si desidera aggiungere (e' possibile utilizzare anche quantita' negative per simulare la rimozione dal magazzino) ed il prezzo singolo. L'ultimo argomento rappresenta una stringa identificativa del fornitore (utili per l'esamina dei log).

Il fornitore e' avviabile tramite il comando ant run.distributore -DcodiceArticolo=XXXXXXX -DdescrizioneArticolo=DESCRIZIONE -DquantitaArticolo=QUANTITA -DprezzoArticolo=PREZZO -DidDistributore=IDFornitore.

Proviamo ad inserire un articolo definito come segue:

  • codiceArticolo=PLAYSTA

  • descrizioneArticolo=console_videogiochi

  • quantitaArticolo=1

  • prezzoArticolo=20.0

Esaminando i log del negozio di shop (/tmp/shop.log) possiamo vedere se l'inserimento va a buon fine. Di seguito un esempio del log:

INFO  isi.shop  - Ricevuta richiesta

INFO  isi.shop  - Validazione XML dell'ordine...

INFO  isi.shop  - [FornitoreEsempio] modifica articolo PLAYSTA con pezzi n. 1 prezzo:20.0

INFO  isi.shop  - [FornitoreEsempio] 1 articoli [PLAYSTA] sono stati aggiunti in magazzino con successo.

INFO  isi.shop  - [FornitoreEsempio] Modifiche effettuate

2.6. analisi/Utilizzo del client acquirente che permette di comprare articoli

Il client http che permette di acquistare articoli viene implementato dalla classe isi.http.AcquirenteClientHTTP. Richiede come input tre argomenti, due dei quali definiscono il codice dell'articolo che si desidera acquistare e la quantita' desiderata (e' possibile utilizzare come codice articolo la keyword * per indicare che si desidera acquistare qualsiasi articolo presente in magazzino. L'ultimo argomento rappresenta una stringa identificativa del cliente (utili per l'esamina dei log).

Il cliente e' avviabile tramite il comando ant run.acquirente -DcodiceArticolo=XXXXXXX -DquantitaArticolo=1 -DidCliente=IDCliente. Il parametro quantitaArticolo non viene interpretato se come codiceArticolo viene utilizzata la keyword *.

Proviamo a comprare l'articolo inserito in magazzino nel precedente paragrafo:

  • codiceArticolo=PLAYSTA

  • quantitaArticolo=1

  • idCliente=A

Esaminando i log del negozio di shop (/tmp/shop.log) possiamo vedere se l'acquisto va a buon fine. Dai log si puo' notare come vi sia un intervallo temporale di 10 secondi che intercorre tra la stampa a video della fattura e il resoconto di acquisto. Questo comportamente, come descritto nei precedenti paragrafi, simula una accettazione del cliente della fattura visualizzata. Di seguito un esempio del log:

 DEBUG isi.shop  - Ricevuta richiesta

 DEBUG isi.shop  - Creazione nuova sessione. Carrello vuoto

 INFO  isi.shop  - Validazione XML dell'ordine...

 INFO  isi.shop  - [A] Richiesta acquisto PLAYSTA: pezzi n. 1
 

 INFO  isi.shop  - [A] ------------- Listino (Articoli) -------------

 INFO  isi.shop  - [A] [codArticolo - descrizione - disponibilita - prezzo]

 INFO  isi.shop  - [A] [PLAYSTA - console_videogiochi - Num. 1 - €. 20.0 ]

 INFO  isi.shop  - [A] --------------------- Fine Listino ----------------------------


 INFO  isi.shop  - [A] --------------------- Fatturazione richiesta ----------------------------

 INFO  isi.shop  - [A] Num.1 di PLAYSTA per un tot di E.20.0

 INFO  isi.shop  - [A] Totale fattura E.20.0

 INFO  isi.shop  - [A] --------------------- Fine fatturazione richiesta ----------------------------


 INFO  isi.shop  - Verifico disponibilita in banca CC n. 123456...

 INFO  isi.shop  - [A] Disponibilita verificata. Articoli acquistabili


 INFO  isi.shop  - [A] Simulazione interazione con il client per richiedere se desidera procedere con l'acquisto....
 ... SLEEP 10 secondi ...


 INFO  isi.shop  - [A] ------------- Resoconto Acquisto -------------

 INFO  isi.shop  - [A] [codArticolo - descrizione - disponibilita - prezzo]

 INFO  isi.shop  - [A] Num.1 di PLAYSTA per un tot di E.20.0

 INFO  isi.shop  - [A] Totale fattura E.20.0

 INFO  isi.shop  - [A] --------------------- Fine Resoconto Acquisto ----------------------------


 INFO  isi.shop  - [A] Commit, salvo modifiche al database

3. Problemi dovuti a transazioni concorrenti

I servizi e i client che sono stati descritti nel precedente capitolo permettono di generare i problemi dovuti a transazioni concorrenti descritte a lezione. Vedremo in ogni paragrafo come tali fenomeni possono avvenire.

3.1. Lost Update

Il Lost Update descrive il fenomeno di perdita di modifiche da parte di una transazione a causa di un aggiornamento effettuato da un'altra transazione. Vediamo come provocare tale fenomeno nel nostro scenario di esempio.

Supponiamo ad esempio di avere un magazzino che possiede un articolo con codice PLAYSTA con disponibilita' uguale alla singola unita'. Tramite il cliente fornitore potete facilmente realizzare questa supposizione (si ricorda che il client e' utilizzabile anche con quantita' negative per simulare rimozione di articoli).

Per provocare il fenomeno di Lost Update simuleremo che due clienti A e B tentino di comprare l'unico articolo con codice PLAYSTA nello stesso istante. Essendoci una singola copia dell'articolo un'applicazione robusta dovrebbe soddisfare la richiesta di un unico cliente. Vedremo invece come la nostra applicazione soddisfa tutti e due le richieste.

  • 1. Acquisto dell'articolo da parte del client A:

    ant run.acquirente -DcodiceArticolo=PLAYSTA -DquantitaArticolo=1 -DidCliente=A

  • 2. Acquisto dell'articolo da parte del client B, il comando deve essere avviato subito dopo aver avviato il precedente (tramite un'altra shell):

    ant run.acquirente -DcodiceArticolo=PLAYSTA -DquantitaArticolo=1 -DidCliente=B

I log riportati evidenziano come entrambi i clienti riescono ad acquistare l'unica playstation disponibile:

  • Il client A inoltra la richiesta di acquisto. Gli viene fornita la fatturazione e il tempo per accettarla:

    
     INFO  isi.shop  - [A] Richiesta acquisto PLAYSTA: pezzi n. 1
     
    
     INFO  isi.shop  - [A] ------------- Listino (Articoli) -------------
    
     INFO  isi.shop  - [A] [codArticolo - descrizione - disponibilita - prezzo]
    
     INFO  isi.shop  - [A] [PLAYSTA - console_videogiochi - Num. 1 - €. 20.0 ]
    
     INFO  isi.shop  - [A] --------------------- Fine Listino ----------------------------
    
    
     INFO  isi.shop  - [A] --------------------- Fatturazione richiesta ----------------------------
    
     INFO  isi.shop  - [A] Num.1 di PLAYSTA per un tot di E.20.0
    
     INFO  isi.shop  - [A] Totale fattura E.20.0
    
     INFO  isi.shop  - [A] --------------------- Fine fatturazione richiesta ----------------------------
     
    
     INFO  isi.shop  - Verifico disponibilita in banca CC n. 123456...
    
     INFO  isi.shop  - [A] Disponibilita verificata. Articoli acquistabili
     
    
     INFO  isi.shop  - [A] Simulazione interazione con il client per richiedere se desidera procedere con l'acquisto....
                            
  • Il client B inoltra la richiesta di acquisto. Gli viene fornita la fatturazione e il tempo per accettarla:

                     
    
     INFO  isi.shop  - [B] Richiesta acquisto PLAYSTA: pezzi n. 1
     
    
     INFO  isi.shop  - [B] ------------- Listino (Articoli) -------------
    
     INFO  isi.shop  - [B] [codArticolo - descrizione - disponibilita - prezzo]
    
     INFO  isi.shop  - [B] [PLAYSTA - console_videogiochi - Num. 1 - €. 20.0 ]
    
     INFO  isi.shop  - [B] --------------------- Fine Listino ----------------------------
    
    
     INFO  isi.shop  - [B] --------------------- Fatturazione richiesta ----------------------------
    
     INFO  isi.shop  - [B] Num.1 di PLAYSTA per un tot di E.20.0
    
     INFO  isi.shop  - [B] Totale fattura E.20.0
    
     INFO  isi.shop  - [B] --------------------- Fine fatturazione richiesta ----------------------------
     
    
     INFO  isi.shop  - Verifico disponibilita in banca CC n. 123456...
    
     INFO  isi.shop  - [B] Disponibilita verificata. Articoli acquistabili
     
    
     INFO  isi.shop  - [B] Simulazione interazione con il client per richiedere se desidera procedere con l'acquisto....
                            
  • Il client A procede con l'acquisto della playstation, infatti riceve il resonto dell'acquisto:

     INFO  isi.shop  - [A] ------------- Resoconto Acquisto -------------
    
     INFO  isi.shop  - [A] [codArticolo - descrizione - disponibilita - prezzo]
    
     INFO  isi.shop  - [A] Num.1 di PLAYSTA per un tot di E.20.0
    
     INFO  isi.shop  - [A] Totale fattura E.20.0
    
     INFO  isi.shop  - [A] --------------------- Fine Resoconto Acquisto ----------------------------
    
    
     INFO  isi.shop  - [A] Commit, salvo modifiche al database
                            
  • Il client B procede con l'acquisto della playstation, infatti riceve il resonto dell'acquisto:

     INFO  isi.shop  - [B] ------------- Resoconto Acquisto -------------
    
     INFO  isi.shop  - [B] [codArticolo - descrizione - disponibilita - prezzo]
    
     INFO  isi.shop  - [B] Num.1 di PLAYSTA per un tot di E.20.0
    
     INFO  isi.shop  - [B] Totale fattura E.20.0
    
     INFO  isi.shop  - [B] --------------------- Fine Resoconto Acquisto ----------------------------
    
    
     INFO  isi.shop  - [B] Commit, salvo modifiche al database
                            

               

3.2. Unrepeatable Read

L'unrepeatable read descrive il fenomeno in cui viene effettuata una analisi inconsistente di dati da parte di una transazione causata da aggiornamenti prodotti da un'altra. Vediamo come provocare tale fenomeno nel nostro scenario di esempio.

Supponiamo ad esempio di avere un magazzino che possiede un articolo con codice PLAYSTA con disponibilita' uguale alla singola unita'. Tramite il cliente fornitore potete facilmente realizzare questa supposizione (si ricorda che il client e' utilizzabile anche con quantita' negative per simulare rimozione di articoli).

Per provocare il fenomeno di Unrepeatable Read simuleremo un cliente A che cerca di comprare tutti gli articoli presenti nel magazzino. Il negozio gli visualizzera' la disponibilita' di un'unico articolo PLAYSTA con disponibilita' uguale alla singola unita' e gli produce una fattura relativa a questo singolo articolo. Durante i 10 secondi che simulano il tempo di accettazione da parte del cliente della fattura visualizzatagli dal negozio, attraverso il client fornitore inseriamo altri 100 articoli con codice PLAYSTA. Il cliente passati i 10 secondi si trovera' un resoconto di fatturazione relativa a 101 playstation!! Sicuramente non sara' felice.

  • 1. Acquisto dell'articolo da parte del client A:

    ant run.acquirente -DcodiceArticolo=* -DidCliente=A

  • 2. Inserimento di 100 unita' dell'articolo PLAYSTA da parte del fornitore Fornitore, il comando deve essere avviato subito dopo aver avviato il precedente (tramite un'altra shell):

    ant run.distributore -DcodiceArticolo=PLAYSTA -DdescrizioneArticolo=console_videogiochi -DquantitaArticolo=100 -DprezzoArticolo=20.0 -DidDistributore=Fornitore

I log riportati evidenziano come il resoconto di fattura finale per il cliente A sia abbastanza piu' oneroso della fattura che aveva accettato:

  • Il client A inoltra la richiesta di acquisto. Gli viene fornita la fatturazione e il tempo per accettarla:

     INFO  isi.shop  - [A] ------------- Listino (Articoli) -------------
    
     INFO  isi.shop  - [A] [codArticolo - descrizione - disponibilita - prezzo]
    
     INFO  isi.shop  - [A] [PLAYSTA - console_videogiochi - Num. 1 - €. 20.0 ]
    
     INFO  isi.shop  - [A] --------------------- Fine Listino ----------------------------
    
    
     INFO  isi.shop  - [A] --------------------- Fatturazione richiesta ----------------------------
    
     INFO  isi.shop  - [A] Num.1 di PLAYSTA per un tot di E.20.0
    
     INFO  isi.shop  - [A] Totale fattura E.20.0
    
     INFO  isi.shop  - [A] --------------------- Fine fatturazione richiesta ----------------------------
     
    
     INFO  isi.shop  - Verifico disponibilita in banca CC n. 123456...
    
     INFO  isi.shop  - [A] Disponibilita verificata. Articoli acquistabili
     
    
     INFO  isi.shop  - [A] Simulazione interazione con il client per richiedere se desidera procedere con l'acquisto....
                              
                            
  • Il fornitore inserisce in magazzino altre 100 unita' dell'articolo:

     INFO  isi.shop  - [Fornitore] modifica articolo PLAYSTA con pezzi n. 100 prezzo:20.0
    
     INFO  isi.shop  - [Fornitore] 100 articoli [PLAYSTA] sono stati aggiunti in magazzino con successo.
    
     INFO  isi.shop  - [Fornitore] Modifiche effettuate
                            
  • Il client A procede con l'acquisto della playstation, ma riceve un resonto dell'acquisto diverso da quello che pensava di aver accettato (2020 euro invece di 20):

     INFO  isi.shop  - [A] ------------- Resoconto Acquisto -------------
    
     INFO  isi.shop  - [A] [codArticolo - descrizione - disponibilita - prezzo]
    
     INFO  isi.shop  - [A] Num.101 di PLAYSTA per un tot di E.2020.0
    
     INFO  isi.shop  - [A] Totale fattura E.2020.0
    
     INFO  isi.shop  - [A] --------------------- Fine Resoconto Acquisto ----------------------------
    
    
     INFO  isi.shop  - [A] Commit, salvo modifiche al database                          
                            

3.3. Phantom row

Questo fenomeno puo' essere descritto come un caso particolare di Dirty Read. Il fenomeno si presenta quando una transazione inserisce o cancella tuple che un'altra transazione dovrebbe logicamente considerare.

Supponiamo ad esempio di avere un magazzino che possiede un articolo con codice PLAYSTA con disponibilita' uguale alla singola unita'. Tramite il cliente fornitore potete facilmente realizzare questa supposizione (si ricorda che il client e' utilizzabile anche con quantita' negative per simulare rimozione di articoli).

Per provocare il fenomeno di Phantom row simuleremo un cliente A che cerca di comprare tutti gli articoli presenti nel magazzino. Il negozio gli visualizzera' la disponibilita' di un'unico articolo PLAYSTA con disponibilita' uguale alla singola unita' e gli produce una fattura relativa a questo singolo articolo. Durante i 10 secondi che simulano il tempo di accettazione da parte del cliente della fattura visualizzatagli dal negozio, attraverso il client fornitore inseriamo un altro articolo con codice TAVOLO4. Il cliente passati i 10 secondi si trovera' un resoconto di fatturazione relativa sia alla playstation che al tavolino, che nel precendete listino non era nemmeno presente.

  • 1. Acquisto dell'articolo da parte del client A:

    ant run.acquirente -DcodiceArticolo=* -DidCliente=A

  • 2. Inserimento di 1 unita' dell'articolo TAVOLO4 da parte del fornitore Fornitore, il comando deve essere avviato subito dopo aver avviato il precedente (tramite un'altra shell):

    ant run.distributore -DcodiceArticolo=TAVOLO4 -DdescrizioneArticolo=tavolo_per_cucina -DquantitaArticolo=1 -DprezzoArticolo=1000.0 -DidDistributore=Fornitore

I log riportati evidenziano come il resoconto di fattura finale per il cliente A sia abbastanza piu' oneroso della fattura che aveva accettato:

  • Il client A inoltra la richiesta di acquisto. Gli viene fornita la fatturazione e il tempo per accettarla:

        INFO  isi.shop  - [A] ------------- Listino (Articoli) -------------
        
        INFO  isi.shop  - [A] [codArticolo - descrizione - disponibilita - prezzo]
        
        INFO  isi.shop  - [A] [PLAYSTA - console_videogiochi - Num. 1 - €. 20.0 ]
        
        INFO  isi.shop  - [A] --------------------- Fine Listino ----------------------------
        
        
        INFO  isi.shop  - [A] --------------------- Fatturazione richiesta ----------------------------
        
        INFO  isi.shop  - [A] Num.1 di PLAYSTA per un tot di E.20.0
        
        INFO  isi.shop  - [A] Totale fattura E.20.0
        
        INFO  isi.shop  - [A] --------------------- Fine fatturazione richiesta ----------------------------
        
        
        INFO  isi.shop  - Verifico disponibilita in banca CC n. 123456...
        
        INFO  isi.shop  - [A] Disponibilita verificata. Articoli acquistabili
        
        
        INFO  isi.shop  - [A] Simulazione interazione con il client per richiedere se desidera procedere con l'acquisto....
                                
                            
  • Il fornitore inserisce in magazzino l'articolo TAVOLO4:

        INFO  isi.shop  - [Fornitore] modifica articolo TAVOLO4 con pezzi n. 1 prezzo:1000.0
        
        INFO  isi.shop  - [Fornitore] 1 articoli [TAVOLO4] sono stati aggiunti in magazzino con successo. 
                                    
        INFO  isi.shop  - [Fornitore] Modifiche effettuate
                            
  • Il client A procede con l'acquisto della playstation, ma riceve un resonto dell'acquisto diverso da quello che pensava di aver accettato (1020 euro invece di 20):

        INFO  isi.shop  - [A] ------------- Resoconto Acquisto -------------
        
        INFO  isi.shop  - [A] [codArticolo - descrizione - disponibilita - prezzo]
        
        INFO  isi.shop  - [A] Num.1 di PLAYSTA per un tot di E.20.0
        
        INFO  isi.shop  - [A] Num.1 di TAVOLO4 per un tot di E.1000.0
        
        INFO  isi.shop  - [A] Totale fattura E.1020.0
        
        INFO  isi.shop  - [A] --------------------- Fine Resoconto Acquisto ----------------------------
        
        
        INFO  isi.shop  - [A] Commit, salvo modifiche al database                                         
                            

4. Esercitazione

L'esercitazione consiste nello studiare un fenomeno alla volta. Per ogni fenomeno deve essere modificato il codice del servizio di vendita (isi.shop.Acquisti), in maniera da risolvere i problemi che avvengono sulle transazioni concorrenti. Le modifiche devono avvenire esaminando i singoli fenomeni; non deve essere fornita un'unica soluzione che risolva entrambi i tre fenomeni obbligatoriamente ma possono essere fornite anche soluzioni diverse per fenomeni diversi. L'importante e' che le soluzioni fornite per ogni fenomeno rispettino i seguenti criteri:

  • Le modifiche devono riguardare solamente il codice del metodo acquistaArticolo

  • Le modifiche non devono modificare i comandi SQL esistenti (se non per l'aggiunta del comando FOR UPDATE)

  • La gestione della connessione deve rimanere in autoCommit=false

  • Deve essere fornita sia una soluzione con lock sia una soluzione con livello di isolamento (se entrambi le soluzioni sono possibile)

  • Deve essere fornita una soluzione con SELECT FOR UPDATE (se possibile)

  • Deve essere fornita la soluzione meno "bloccante"

  • Deve essere fornita una spiegazione per le soluzioni adottate

4.1. Soluzione: Lost Update

4.1.1. Lock

Il lock in lettura (Share) non e' utilizzabile in un contesto di Lost Update poiche' anche se permette ad entrambi i clienti A e B di leggere la tabella listino e a solo ad uno dei due di aggiornarla, provoca la terminazione con errore della transazione dell'altro cliente in seguito al rilevamento del deadlock da parte del DBMS. La soluzione consiste quindi nell'utilizzare il lock in scrittura anche se poco efficente poiche' di fatto serializza l'esecuzione dei due clienti.

Di seguito viene evidenziato il pezzo di codice del metodo acquistaArticolo modificato per l'acquisizione del lock in scrittura:

    // gestisco la transazione applicativamente
    connectionDB.setAutoCommit(false);
    
    
    log.info("Acquisizione lock in exclusive mode...");
    String sqlLock = "LOCK TABLE "+CostantiDatabase.LISTINO+" IN EXCLUSIVE MODE";
    pstmt = connectionDB.prepareStatement(sqlLock);
    pstmt.execute();
    pstmt.close();
    log.info("Acquisizione lock in exclusive mode effettuata.");
    
    
    // Visualizzo listino degli articoli disponibili
    Vector<Articolo> articoliDisponibili = readArticoliDisponibiliInMagazzino(idCliente, connectionDB);

I log evidenziano come il cliente B rimane bloccato sull'acquisizione del lock in scrittura fino a che il cliente A non termina la transazione

                      
    ....
                            
    INFO  isi.shop  - [B] Acquisizione lock in exclusive mode...
    
    ....
    
    INFO  isi.shop  - [A] ------------- Resoconto Acquisto -------------
    
    INFO  isi.shop  - [A] [codArticolo - descrizione - disponibilita - prezzo]
    
    INFO  isi.shop  - [A] Num.1 di PLAYSTA per un tot di E.20.0
    
    INFO  isi.shop  - [A] Totale fattura E.20.0
    
    INFO  isi.shop  - [A] --------------------- Fine Resoconto Acquisto ----------------------------
    
    INFO  isi.shop  - [A] Commit, salvo modifiche al database
    
    
    INFO  isi.shop  - [B] Acquisizione lock in exclusive mode effettuata.
    
    ....
                    

4.1.2. Select For Update

Il lock in scrittura, esaminato nel precedente paragrafo, anche se risolve il problema non e' la migliore soluzione poiche' mette un lock su tutta la tabella anche per operazioni di lettura.

Con il comando SELECT ... FOR UPDATE e' possibile in genere risolvere i problemi di lost update, con una efficenza migliore rispetto al lock in scittura. Questo poichè è possibile impostare un lock in scrittura solo sulle righe interessate senza bloccare di conseguenza tutta la tabella. La considerazione non e' valida purtroppo nel nostro esempio poiche' anche se il comando FOR UPDATE viene inserito in fondo alla select utilizzata dal metodo readArticoliDisponibiliInMagazzino, di fatto si richiede un lock su ciascuna riga esistente nella tabella poiche' tale metodo non possiede condizioni che limitano la ricerca a determinate righe. Si arriverebbe di fatto ad un lock in scrittura su tutta la tabella.

In definitiva il select for update risulta una soluzione anche se non migliore del lock esclusivo. Di seguito viene evidenziato il pezzo di codice del metodo readArticoliDisponibiliInMagazzino modificato per l'utilizzo della select for update:

    private Vector<Articolo> readArticoliDisponibiliInMagazzino(String idCliente,Connection connectionDB) throws Exception{
        
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        Vector<Articolo> articoli = new Vector<Articolo>();
        try{
             
            log.info(idCliente+" ------------- Listino (Articoli) -------------");
            String sqlQuery = "SELECT * FROM "+CostantiDatabase.LISTINO +" FOR UPDATE";
            pstmt = connectionDB.prepareStatement(sqlQuery);
        ...

I log evidenziano come il cliente B rimane bloccato sulla query utilizzata per la lettura del magazzino fino a che il cliente A non termina la transazione

                        
    ....
    
    INFO  isi.shop  - [B] ------------- Listino (Articoli) -------------
    
    ....
    
    INFO  isi.shop  - [A] ------------- Resoconto Acquisto -------------
    
    INFO  isi.shop  - [A] [codArticolo - descrizione - disponibilita - prezzo]
    
    INFO  isi.shop  - [A] Num.1 di PLAYSTA per un tot di E.20.0
    
    INFO  isi.shop  - [A] Totale fattura E.20.0
    
    INFO  isi.shop  - [A] --------------------- Fine Resoconto Acquisto ----------------------------
    
    INFO  isi.shop  - [A] Commit, salvo modifiche al database
    
    
    INFO  isi.shop  - [B] [codArticolo - descrizione - disponibilita - prezzo]
    
    INFO  isi.shop  - [B] [PLAYSTA - console_videogiochi - Num. 0 - €. 20.0 ]
    
    INFO  isi.shop  - [B] --------------------- Fine Listino ----------------------------
    
    ....
                    

4.1.3. Livelli di isolamento

Come abbiamo visto a lezione solo il livello di isolamento SERIALIZABLE permette di risolvere il fenomeno del Lost Update. Il livello di isolamento SERIALIZABLE permette che una semplice lettura di un dato da parte di una transazione provochi il blocco degli aggiornamento effettuati da tutte le altre transazioni. Se due transazioni concorrenti tentano di effettuare entrambe l'aggiornamento della base di dati, solo una delle due vi riesce e l'altra verra' terminata con un errore di conflitto di serializzazione generato dal DBMS.

Le modalita' di errore avvengono in maniera simile a quanto succede con il lock in lettura (shared). La differenza sostanziale con il lock in lettura e' che una successiva lettura dei dati da parte della transazione che ha subito il conflitto di serializzazione sara' consistente e cioe' leggera' la base di dati modificata dalla transazione terminata correttamente. Questo permette di applicare una gestione dell'errore all'interno del metodo in modo da risolvere correttamente il problema del Lost Update. Di seguito viene evidenziato il pezzo di codice del metodo acquistaArticolo modificato per l'utilizzo del livello di isolamento e per la gestione dei conflitti di serializzazione.

    // Ottengo connessione al database
    connectionDB = DriverManager.getConnection( 
        CostantiDatabase.CONNECTION_URL,
        CostantiDatabase.USERNAME_ACCESSO_DATABASE,
        CostantiDatabase.PASSWORD_ACCESSO_DATABASE );
    if(connectionDB==null){
        log.error("Connection is null");
    }
    
    
    // imposto livello di isolamento SERIALIZABLE
    connectionDB.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
                    
                            
    // gestisco la transazione applicativamente
    connectionDB.setAutoCommit(false);
    
    
    boolean serializzazioneConConflitti = true;
    boolean first = true;
    while(serializzazioneConConflitti){
    
        try{
        
            if(!first){
                log.info(idCliente+" Ci scusiamo per il disagio ma il sistema non e' stato in grado di gestire la vostra richiesta.");
                log.info(idCliente+" E' possibile ritentare l'acquisto ma e' necessario la ri-conferma della fattura di acquisto.");
            }
            first = false;
            
            
            // Visualizzo listino degli articoli disponibili
            Vector<Articolo> articoliDisponibili = readArticoliDisponibiliInMagazzino(idCliente, connectionDB);
                
            ....
                
                
            // Commit delle operazioni effettuate sul database	
            log.info(idCliente+" Commit, salvo modifiche al database");
            connectionDB.commit();
            
            
            // Marco transazione terminata senza errori di deadlock
            serializzazioneConConflitti = false;
        
        }catch(SQLException eSQL){
            
            carrello.clear();
            
            // rollback
            try{
                log.error(idCliente+" Rollback, annullo modifiche al database: "+eSQL.getMessage());
                connectionDB.rollback();
            }catch(Exception eRollback){}
            
            if(eSQL.getMessage().contains("could not serialize access due to concurrent update")){
                // conflitto di serializzazione, devo riprovare ad eseguire interamente la logica applicativa
                serializzazioneConConflitti = true;
            }else{
                // errore non dovuto a conflitti di serializzazione
                // gestione applicativa normale dell'errore
                serializzazioneConConflitti = false;
                throw eSQL; // rilancio eccezione
            }
            
        }
    }
                

I log evidenziano come il cliente B non riesce a terminare la transazione per un conflitto di serializzazione. Il sistema si scusa per il disagio allocato e tenta di riapplicare l'ordine del cliente accorgendosi pero' della mancanza di disponibilita' dell'articolo richiesto.

                        
    ....
                        
    INFO  isi.shop  - [B] ------------- Resoconto Acquisto -------------
    
    INFO  isi.shop  - [B] [codArticolo - descrizione - disponibilita - prezzo]
    
    INFO  isi.shop  - [B] Num.1 di PLAYSTA per un tot di E.20.0
    
     ERROR isi.shop  - [B] Rollback, annullo modifiche al database: ERROR: could not serialize access due to concurrent update
    
    INFO  isi.shop  - [B] Ci scusiamo per il disagio ma il sistema non e' stato in grado di gestire la vostra richiesta.
    
    INFO  isi.shop  - [B] E' possibile ritentare l'acquisto ma e' necessario la ri-conferma della fattura di acquisto.
    
    INFO  isi.shop  - [B] ------------- Listino (Articoli) -------------
    
    INFO  isi.shop  - [B] [codArticolo - descrizione - disponibilita - prezzo]
    
    INFO  isi.shop  - [B] [PLAYSTA - console_videogiochi - Num. 0 - €. 20.0 ]
    
    INFO  isi.shop  - [B] --------------------- Fine Listino ----------------------------
    
    
    INFO  isi.shop  - [B] --------------------- Fatturazione richiesta ----------------------------
    
    ERROR isi.shop  - [B] Prodotto [PLAYSTA] non esistente in magazzino
    
    INFO  isi.shop  - [B] --------------------- Fine fatturazione richiesta ----------------------------
                    

4.2. Soluzione: Unrepeatable Read

4.2.1. Lock

Sia il lock in lettura (Share) che quello in scrittura (Exclusive) sono utilizzabili per risolvere il problema. Chiaramente quello in lettura e' piu' efficente.

Il cliente A acquisendo il lock in lettura sulla tabella degli articoli non permette al fornitore di aggiornare la quantita' di playstation presenti nel magazzino fino a che la sua transazione non sia terminata.

Di seguito viene evidenziato il pezzo di codice del metodo acquistaArticolo modificato per l'acquisizione del lock in scrittura:

    // gestisco la transazione applicativamente
    connectionDB.setAutoCommit(false);
    
    
    log.info("Acquisizione lock in share mode...");
    String sqlLock = "LOCK TABLE "+CostantiDatabase.LISTINO+" IN SHARE MODE";
    pstmt = connectionDB.prepareStatement(sqlLock);
    pstmt.execute();
    pstmt.close();
    log.info("Acquisizione lock in share mode effettuata.");
    
    
    // Visualizzo listino degli articoli disponibili
    Vector<Articolo> articoliDisponibili = readArticoliDisponibiliInMagazzino(idCliente, connectionDB);

I log evidenziano come il fornitore B rimane bloccato sull'inserimento dei nuovi articoli fino a che il cliente A non termina la transazione di acquisto.

  • Il client A inoltra la richiesta di acquisto. Gli viene fornita la fatturazione e il tempo per accettarla:

                          
         INFO  isi.shop  - [A] Acquisizione lock in share mode...
        
         INFO  isi.shop  - [A] Acquisizione lock in share mode effettuata.
        
         INFO  isi.shop  - [A] ------------- Listino (Articoli) -------------
        
         INFO  isi.shop  - [A] [codArticolo - descrizione - disponibilita - prezzo]
        
         INFO  isi.shop  - [A] [PLAYSTA - console_videogiochi - Num. 1 - €. 20.0 ]
        
         INFO  isi.shop  - [A] --------------------- Fine Listino ----------------------------
        
        
         INFO  isi.shop  - [A] --------------------- Fatturazione richiesta ----------------------------
        
         INFO  isi.shop  - [A] Num.1 di PLAYSTA per un tot di E.20.0
        
         INFO  isi.shop  - [A] Totale fattura E.20.0
        
         INFO  isi.shop  - [A] --------------------- Fine fatturazione richiesta ----------------------------
        
         INFO  isi.shop  - Verifico disponibilita in banca CC n. 123456...
        
         INFO  isi.shop  - [A] Disponibilita verificata. Articoli acquistabili
        
         INFO  isi.shop  - [A] Simulazione interazione con il client per richiedere se desidera procedere con l'acquisto....   
                                
  • Il Fornitore cerca di inserire nel magazzino 100 nuove unita' dell'articolo PLAYSTA, l'inserimento rimane bloccato:

                          
        [http-0.0.0.0-8080-97] INFO  isi.shop  - [Fornitore] modifica articolo PLAYSTA con pezzi n. 100 prezzo:20.0  
                                
  • Il client A procede con l'acquisto della playstation, infatti riceve il resonto dell'acquisto:

        INFO  isi.shop  - [A] ------------- Resoconto Acquisto -------------
        
        INFO  isi.shop  - [A] [codArticolo - descrizione - disponibilita - prezzo]
        
        INFO  isi.shop  - [A] Num.1 di PLAYSTA per un tot di E.20.0
        
        INFO  isi.shop  - [A] Totale fattura E.20.0
        
        INFO  isi.shop  - [A] --------------------- Fine Resoconto Acquisto ----------------------------
        
        
        INFO  isi.shop  - [A] Commit, salvo modifiche al database
                                
  • Il Fornitore riesce ad inserire correttamente nel magazzino 100 nuove unita' dell'articolo PLAYSTA:

            
        INFO  isi.shop  - [Fornitore] 100 articoli [PLAYSTA] sono stati aggiunti in magazzino con successo.
        
        INFO  isi.shop  - [Fornitore] Modifiche effettuate                 
                                

4.2.2. Select For Update

L'istruzione e' utilizzabile per risolvere il fenomeno dell'unrepeatable read, poiche' viene impostato un lock esclusivo su ciascuna riga letta. Nessun altro cliente/fornitore riesce a leggere/modificare tale dato. Per valutare se sia meglio questa soluzione o il lock in lettura, deve essere esaminato il contesto applicativo in cui ci troviamo. Se il contesto in cui ci troviamo possiede molti clienti che utilizzano la tabella solo per lettura e solo uno/pochi clienti che ne modificano i dati, la soluzione con il lock in lettura e' migliore. Se il contesto possiede invece molti clienti che alterano la tabella, il comando SELECT ... FOR UPDATE e' migliore poiche' permette ad ogni cliente di leggere e modificare solo i proprio dati (con l'eccezione dei dati che interessano contemporaneamente a piu' clienti).

Per come e' strutturato il nostro esempio non e' possibile ottenere una efficenza migliore rispetto al lock in lettura. Se il comando FOR UPDATE viene inserito in fondo alla select utilizzata dal metodo readArticoliDisponibiliInMagazzino, di fatto si richiederebbe un lock esclusivo su ciascuna riga esistente nella tabella poiche' tale metodo non possiede condizioni che limitano la ricerca a determinate righe.

In definitiva il select for update risulta una soluzione anche se non migliore del lock in lettura. Nella Sezione 4.1.2, «Select For Update» viene evidenziato il pezzo di codice del metodo readArticoliDisponibiliInMagazzino modificato per l'utilizzo della select for update.

I log evidenziano come il fornitore B rimane bloccato sull'inserimento dei nuovi articoli fino a che il cliente A non termina la transazione di acquisto come descritto nella Sezione 4.2.1, «Lock».

4.2.3. Livelli di isolamento

Come abbiamo visto a lezione solo i livelli di isolamento UNREPEATABLE_READ e SERIALIZABLE permettono di risolvere il fenomeno. Teoricamente quindi dovrebbe essere utilizzato il livello UNREPEATABLE_READ poiche' rappresenta la soluzione meno bloccante.

Sempre a lezione e' stato spiegato come le implementazioni dei DBMS di fatto forniscono solo due livelli distinti che corrispondono a READ_COMMITTED e SERIALIZABLE. La soluzione ricade quindi nella scelta del livello di isolamento SERIALIZABLE. Il nostro codice dovra' quindi gestire i conflitti di accesso che possono avvenire con l'utilizzo di tale livello di isolamento. Se due transazioni concorrenti tentano di effettuare entrambe l'aggiornamento della base di dati, solo una delle due vi riesce e l'altra verra' terminata con un errore di conflitto di serializzazione generato dal DBMS.

Nella Sezione 4.1.3, «Livelli di isolamento» viene evidenziato il pezzo di codice del metodo readArticoliDisponibiliInMagazzino modificato per l'utilizzo del livello di isolamento serializable.

I log evidenziano come il fornitore B rimane bloccato sull'inserimento dei nuovi articoli fino a che il cliente A non termina la transazione di acquisto come descritto nella Sezione 4.2.1, «Lock».

4.3. Soluzione: Phantom Row

4.3.1. Lock

Sia il lock in lettura (Share) che quello in scrittura (Exclusive) sono utilizzabili per risolvere il problema. Chiaramente quello in lettura e' piu' efficente.

Il cliente A acquisendo il lock in lettura sulla tabella degli articoli non permette al fornitore di aggiungere il nuovo articolo (TAVOLO4) nel magazzino fino a che la sua transazione non sia terminata.

Nella Sezione 4.2.1, «Lock» viene evidenziato il pezzo di codice del metodo acquistaArticolo modificato per l'utilizzo il lock in lettura.

I log evidenziano come il fornitore B rimane bloccato sull'inserimento dei nuovi articoli fino a che il cliente A non termina la transazione di acquisto.

  • Il client A inoltra la richiesta di acquisto. Gli viene fornita la fatturazione e il tempo per accettarla:

                          
                                    INFO  isi.shop  - [A] Acquisizione lock in share mode...
                                    
                                    INFO  isi.shop  - [A] Acquisizione lock in share mode effettuata.
                                    
                                    INFO  isi.shop  - [A] ------------- Listino (Articoli) -------------
                                    
                                    INFO  isi.shop  - [A] [codArticolo - descrizione - disponibilita - prezzo]
                                    
                                    INFO  isi.shop  - [A] [PLAYSTA - console_videogiochi - Num. 1 - €. 20.0 ]
                                    
                                    INFO  isi.shop  - [A] --------------------- Fine Listino ----------------------------
                                    
                                    
                                    INFO  isi.shop  - [A] --------------------- Fatturazione richiesta ----------------------------
                                    
                                    INFO  isi.shop  - [A] Num.1 di PLAYSTA per un tot di E.20.0
                                    
                                    INFO  isi.shop  - [A] Totale fattura E.20.0
                                    
                                    INFO  isi.shop  - [A] --------------------- Fine fatturazione richiesta ----------------------------
                                    
                                    INFO  isi.shop  - Verifico disponibilita in banca CC n. 123456...
                                    
                                    INFO  isi.shop  - [A] Disponibilita verificata. Articoli acquistabili
                                    
                                    INFO  isi.shop  - [A] Simulazione interazione con il client per richiedere se desidera procedere con l'acquisto....   
                                
  • Il Fornitore cerca di inserire nel magazzino il nuovo articolo TAVOLO4, l'inserimento rimane bloccato:

                          
                                    [http-0.0.0.0-8080-97] INFO  isi.shop  - [Fornitore] modifica articolo TAVOLO4 con pezzi n. 1 prezzo:1000.0
                                
  • Il client A procede con l'acquisto della playstation, infatti riceve il resonto dell'acquisto:

                                    INFO  isi.shop  - [A] ------------- Resoconto Acquisto -------------
                                    
                                    INFO  isi.shop  - [A] [codArticolo - descrizione - disponibilita - prezzo]
                                    
                                    INFO  isi.shop  - [A] Num.1 di PLAYSTA per un tot di E.20.0
                                    
                                    INFO  isi.shop  - [A] Totale fattura E.20.0
                                    
                                    INFO  isi.shop  - [A] --------------------- Fine Resoconto Acquisto ----------------------------
                                    
                                    
                                    INFO  isi.shop  - [A] Commit, salvo modifiche al database
                                
  • Il Fornitore riesce ad inserire correttamente nel magazzino il nuovo articolo TAVOLO4:

            
                                    INFO  isi.shop  - [Fornitore] 1 articoli [TAVOLO4] sono stati aggiunti in magazzino con successo.
                                    
                                    INFO  isi.shop  - [Fornitore] Modifiche effettuate                 
                                

4.3.2. Select For Update

L'istruzione non e' utilizzabile per risolvere il fenomeno del phantom row poiche' tale fenomeno influenza altre righe della tabella (non ancora lette dalla transazione del cliente).

4.3.3. Livelli di isolamento

Come abbiamo visto a lezione solo il livello di isolamento SERIALIZABLE permette di risolvere il fenomeno.

Nella Sezione 4.1.3, «Livelli di isolamento» viene evidenziato il pezzo di codice del metodo readArticoliDisponibiliInMagazzino modificato per l'utilizzo del livello di isolamento serializable.

I log evidenziano come il fornitore B rimane bloccato sull'inserimento del nuovo articolo fino a che il cliente A non termina la transazione di acquisto come descritto nella Sezione 4.3.1, «Lock».

Footer BGFooter BG
Tito Flagella - © 2007-2015