Home   |   Archivio   |   Chi siamo   |   Contatti
Giovanni Tommasi
Object Pooling
Parte II: esempi concreti, i connection pool
di
Giovanni Tommasi
Dopo l'introduzione teorica all'object pooling, vediamo un caso concreto: un pool di connessioni usato nell'ambito di una servlet.

Il problema delle connessioni nelle applicazioni web è uno di quelli che richiedono soluzioni di alto livello, sia dal punto di vista della progettazione che della realizzazione.

Supponiamo di avere una servlet che interroga un database, e vediamo dove e come si può affrontare il problema della connessione. Ci sono sicuramente due possibilità:

  • l'oggetto java.sql.Connection viene dichiarato come attributo e poi istanziato nell'init(), oppure la prima volta che si entra nel service(), e poi chiuso nel destroy();
  • l'oggetto java.sql.Connection viene dichiarato e istanziato nel service() prima di effettuare le operazioni SQL sul DB, e poi chiuso sempre nel service() alla fine di queste.

Ambedue queste possibilità hanno dei pro e dei contro. Nel primo caso, la connessione viene aperta e chiusa una volta sola, e questo comporta senz'altro una ottimizzazione delle prestazioni, poiché l'apertura di una connessione è una cosa pesante, soprattutto sul web. D'altro canto, però, la nostra servlet dovrà implementare SingleThreadModel, oppure il service() dovrà essere dichiarato synchronized, perché Connection, se dichiarata come attributo della classe, è una risorsa condivisa da tutti i thread che vengono lanciati.

Nel secondo caso invece non abbiamo alcun problema di accesso concorrente, perché la dichiarazione di Connection sta dentro il metodo service(), per cui vengono create tante connessioni quanti sono i thread lanciati. Tuttavia, la connessione viene aperta e chiusa tutte le volte che si effettua una richiesta, e anche questo è un carico di lavoro molto pesante per il server.

Perché usare un pool di connessioni

L'uso di un pool di connessioni consente di sfruttare la parte migliore di ambedue queste tecniche senza doverne sopportare anche gli svantaggi: istanziamo l'oggetto che ci serve una volta sola, e lo usiamo tutte le volte che ci serve.

La differenza rispetto al primo caso è che non facciamo l'istanza di java.sql.Connection, ma di un oggetto, il pool appunto, che gestisce le nostre richieste di connessione. La connessione già pronta viene ottenuta nel service(), come nel secondo caso, e alla fine di esso viene restituita, non chiusa.

Per fare questo abbiamo bisogno di scrivere una classe che avvolga un oggetto Connection e alcuni altri attributi che ci serviranno per avere ragguagli sullo stato dell'oggetto.

Usando la tecnica descritta nel precedente articolo possiamo scrivere una classe simile a questa:

import java.sql.*;

public class PoolableConnection {
  private Connection con = null;
  private boolean locked = false;
  private long calledTime = 0;

  public PoolableConnection(Connection aConnection) {
    if (aConnection != null) {
      con = aConnection;
    }
  }

  public Connection getConnection() {
    return con;
  }

  public void setLocked(boolean lock) {
    locked = lock;
    calledTime = System.currentTimeMillis();
  }

  public void close() throws SQLException {
    try {
      con.close();
    } catch (SQLException sqle) {
      System.err.println(sqle.getMessage());
    }
  }
}

PoolableConnection è scritto appositamente per essere usato da un pool. La classe ConnectionPool, che è il pool vero e proprio, deve in primo luogo predisporre le connessioni e poi inserirle in una lista, ad esempio un Vector.

Struttura del pool

La classe ConnectionPool definisce una serie di variabili necessarie alla connessione con un database via JDBC:

public class ConnectionPool {
  private String driver;
  private String url;
  private int size;
  private int pctIncrease;
  private String user;
  private String password;
  private Vector pool;
  private java.sql.Driver drv;
  private long maxTimeInUse = 10000;
}

La prima funzione di cui questa classe ha bisogno è un metodo per inizializzare il pool:

public synchronized void initializePool() throws Exception {
  if (driver == null) {
    throw new Exception("Nome driver jdbc non specificato");
  }

  try {
    drv = (Driver) Class.forName(driver).newInstance();
    DriverManager.registerDriver(drv);

    for (int i = 0; i < size; i++) {
      Connection con = createConnection();
      if (con != null) {
        PoolableConnection pcon = new PoolableConnection(con);
        addConnection(pcon);
      }
    }
  } catch (Exception e) {
    throw new Exception("Eccezione in initializeConnection: " + e.getMessage());
  }
}

Dopo aver fatto l'istanza del pool, la nostra applicazione dovrà valorizzare le variabili del pool e poi chiamare initializePool(). A questo punto il pool è pronto per essere usato.

Ottenere e rilasciare una connessione

Per ottenere un oggetto Connection dal pool, è sufficiente invocare il metodo getConnection():

public synchronized Connection getConnection() throws Exception {
  PoolableConnection pcon = null;

  for (int i = 0; i < pool.size(); i++) {
    pcon = (PoolableConnection) pool.elementAt(i);
    if (!pcon.isLocked()) {
      pcon.setLocked(true);
      return pcon.getConnection();
    }
  }

  boolean expand = (size + (size / 100 * pctIncrease) < pool.size());
  if (expand) {
    expandPool();
  }
}

Se le connessioni sono tutte impegnate, getConnection() verifica se gli elementi del vettore sono in numero minore rispetto a size più la percentuale massima di incremento. Se è possibile, il pool viene espanso creando una nuova connessione e aggiungendola al vettore.

La parte più delicata sta nel rilascio della connessione. Qualunque ipotesi di impossibilità di rilasciare la connessione si verifichi, noi dobbiamo sapere come e quando correggerla. Vediamo cosa succede nel metodo releaseConnection():

public synchronized void releaseConnection(Connection con) {
  boolean foundAndKilled = false;

  for (int i = 0; i < pool.size(); i++) {
    PoolableConnection pcon = (PoolableConnection) pool.elementAt(i);
    if (pcon.getConnection() == con) {
      pcon.setLocked(false);
      foundAndKilled = true;
      break;
    }
  }

  if (!foundAndKilled) {
    new ControlThread(maxTimeInUse, pool);
  }
}

Se dopo aver letto tutto il vettore questo metodo non riesce a rilasciare il lock sull'oggetto che gli è stato passato, invoca un thread che legge di nuovo tutto il vettore, alla ricerca di quelle connessioni per le quali il tempo di impiego si sia prolungato oltre quanto ci si aspetta.

Questa condizione provoca una forzatura di setLocked(false), in modo che l'oggetto ritorni ad essere disponibile e libero.

Considerazioni finali

Il connection pool consente quindi di ridurre drasticamente il costo di apertura e chiusura delle connessioni, mantenendo al tempo stesso la possibilità di servire più richieste concorrenti senza condividere in modo insicuro un'unica connessione globale.

Il prezzo da pagare è una maggiore complessità di progetto: bisogna infatti gestire con cura il locking, l'espansione del pool, il rilascio corretto delle risorse e i casi patologici in cui una connessione rimane bloccata troppo a lungo.

Resta però un esempio molto concreto di come la tecnica dell'object pooling possa trasformarsi, nella pratica, in uno strumento estremamente utile.

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