In ogni applicazione che prevede l'interazione con gli utenti, è necessario ridurre il tempo di caricamento delle informazioni richieste. Utilizzando un sistema di cache, è possibile ottimizzare il tempo di caricamento dei dati e ottenere, quindi, prestazioni migliori.
Immaginiamo, ad esempio, un sistema che utilizza un database per memorizzare i dati, ogni richiesta dell'utente corrisponde a un'interrogazione al DBMS. Il tempo necessario per effettuare la connessione, e l'interrogazione sulla base dati, potrebbe influire notevolmente sul tempo di caricamento della pagina.
Sfruttando la filosofia che sta alla base di un sistema di caching, invece, è possibile ridurre i tempi di attesa dell'utente. È possibile effettuare un salvataggio temporaneo in memoria di una serie di dati, che hanno determinate caratteristiche di variabilità e che hanno un'alta frequenza di utilizzo. Utilizzare la cache, infatti, non vuol dire avere a disposizione tutte le informazioni disponibili e censite nel sistema, ma solo parte di esse. Quando viene richiesta un'informazione, non c'è nessuna certezza che i dati si trovino all'interno della cache, ma conviene comunque fare un tentativo per verificarne l'eventuale esistenza prima di leggerli dal DBMS. Solo qualora i dati non siano presenti, è necessario leggerli dal database e caricarli all'interno della cache. Le successive richieste degli utenti che riguardano i dati risulteranno sicuramente più veloci.
Naturalmente la cache deve avere dimensioni ridotte per permettere ricerche più veloci. Sono disponibili moltissimi algoritmi che permettono di individuare l'elemento da eliminare quando la cache è piena.
In questo articolo analizziamo i concetti fondamentali di JCS, Java Cache System, una libreria opensource sviluppata dalla Apache Software Foundation, che permette di mettere a punto un meccanismo di caching all'interno delle proprie applicazioni java.
L'ultima versione disponibile è la 1.3. È possibile scaricare anche direttamente il jar da includere nelle proprie applicazioni. Per utilizzare JCS all'interno delle proprie applicazioni, è necessario includere nel classpath anche le seguenti librerie dipendenti:
Gli elementi all'interno della cache vengono suddivisi in regioni, ognuna delle quali può gestire differenti tipologie di elementi. A ciascuna regione può essere associato un plugin, di seguito denominato "ausiliare". JCS mette a disposizione dello sviluppatore diverse tipologie di ausiliari ognuno dei quali ha differenti implementazioni e degli attributi specifici configurabili. Per ulteriori approfondimenti è consigliato fare riferimento alla documentazione ufficiale.
La configurazione di un sistema di caching è affidata a un file di testo: cache.ccf. È possibile utilizzare anche altre tipologie di file ma queste, sono al di là del campo di applicazione di questa semplice guida introduttiva.
Supponiamo di avere un catalogo di prodotti nel nostro negozio online. Ogni volta che un utente accede alla lista di prodotti, oppure al dettaglio di ciascuno di questi, occorre accedere al DB per ottenere le informazioni da visualizzare. È molto probabile che i prodotti disponibili non vengano aggiornati troppo spesso e che gli utenti visualizzino soprattutto la pagina principale. È possibile quindi aggiungere alla cache i prodotti più aggiornati. Quando l'amministratore del sistema aggiungerà, modificherà o cancellerà uno o più prodotti da un ipotetico pannello di back-end, ci preoccuperemo di aggiornare i dati all'interno della nostra cache.
Il nostro file cache.ccf sarà il seguente:
#REGIONE PRODOTTI
jcs.region.prodotti=PRODOTTI
jcs.region.prodotti.cacheattributes=org.apache.jcs.engine.CompositeCacheAttributes
jcs.region.prodotti.cacheattributes.MaxObjects=1000
jcs.region.prodotti.cacheattributes.MemoryCacheName=org.apache.jcs.engine.memory.lru.LRUMemoryCache
jcs.region.prodotti.cacheattributes.UseMemoryShrinker=false
jcs.region.prodotti.cacheattributes.MaxMemoryIdleTimeSeconds=3600
jcs.region.prodotti.cacheattributes.ShrinkerIntervalSeconds=60
jcs.region.prodotti.elementattributes=org.apache.jcs.engine.ElementAttributes
jcs.region.prodotti.elementattributes.IsEternal=false
#AUXILIARY PRODOTTI
jcs.auxiliary.PRODOTTI=org.apache.jcs.auxiliary.disk.indexed.IndexedDiskCacheFactory
jcs.auxiliary.PRODOTTI.attributes=org.apache.jcs.auxiliary.disk.indexed.IndexedDiskCacheAttributes
jcs.auxiliary.PRODOTTI.attributes.DiskPath=D:ProjectsDPCJCStemp
jcs.auxiliary.PRODOTTI.attributes.MaxPurgatorySize=10000000
jcs.auxiliary.PRODOTTI.attributes.MaxKeySize=1000000
jcs.auxiliary.PRODOTTI.attributes.MaxRecycleBinSize=5000
jcs.auxiliary.PRODOTTI.attributes.OptimizeAtRemoveCount=300000
jcs.auxiliary.PRODOTTI.attributes.ShutdownSpoolTimeLimit=60
Nella prima riga del file abbiamo definito una regione chiamata PRODOTTI. Mediante l'attributo cacheattributes
definiamo le impostazioni generali utilizzate dalla nostra cache. Nel nostro esempio abbiamo utilizzato la classe CompositeCacheAttributes
che permette di definire i seguenti parametri principali:
- MaxObjects
- MemoryCacheName
LRUMemoryCache
- UseMemoryShrinker
- MaxMemoryIdleTimeSeconds
- ShrinkerIntervalSeconds
- Sono previsti anche attributi relativi agli elementi appartenenti alla regione che sono i seguenti:
- IsEternal
- MaxLifeSeconds
È possibile anche definire delle impostazioni di default. Tutte le regioni ereditano le impostazioni di default qualora le proprietà non vengano esplicitamente definite.
Nell'esempio abbiamo associato alla nostra regione l'ausiliare IndexedDiskCacheFactory
, che permette di effettuare lo spool (scrittura) sul disco degli elementi prima che questi vengano eliminati dalla cache. Questo meccanismo permette di memorizzare gli elementi della cache sul disco e le chiavi di ciascuno di essi in memoria permettendo, quindi, di verificare la presenza di un elemento molto più velocemente.
Quando un elemento viene rimosso dalla cache viene subito copiato in una "regione" della memoria chiamata "purgatorio" per evitare di attendere l'interrupt del sistema che abilita la scrittura sul disco. Può capitare che un elemento venga richiesto ma non è presente nella cache. In questo caso viene prima verificata la presenza nel "purgatorio". In caso positivo l'elemento viene riportato nella cache.
I principali attributi configurabili per questo ausiliare sono i seguenti:
- DiskPath
- MaxPurgatorySize
- MaxKeySize
- MaxRecycleBinSize
Adesso analizziamo la classe ProdottoManager che permette di gestire i nostri prodotti. La classe implementa il pattern singleton poiché è necessario avere un'unica istanza della classe nel sistema.
Le operazioni principali da gestire sono le seguenti:
- Creazione della cache
getIstance
JCS.getInstance(regione)
- Inserimento di un elemento nella cache
cache.put(chiave, istanzaOggetto)
- Lettura di un elemento dalla cache
Listato 1. Permette di gestire i prodotti
public class ProdottoManager {
private final String CACHE_NAME = "prodotti";
private JCS cache = null;
private static ProdottoManager instance = null;
private Log log = LogFactory.getLog(this.getClass());
private ProdottoManager() {
try {
setCache(JCS.getInstance(CACHE_NAME));
} catch (CacheException e) {
log.error(e);
}
}
public static ProdottoManager getInstance() {
if (instance == null)
instance = new ProdottoManager();
return instance;
}
public JCS getCache() {
return cache;
}
public void setCache(JCS cache) {
this.cache = cache;
}
public ProdottoVO get(int id) {
ProdottoVO prodotto = (ProdottoVO) cache.get("id_" + id);
if (null == prodotto) {
prodotto = load(id);
log.info("oggetto id_" + id + " letto dal db");
try {
cache.put("id_" + id, prodotto);
log.info("oggetto id_" + id + " inserito nella cache");
} catch (CacheException e) {
log.error(e);
}
}
return prodotto;
}
private ProdottoVO load(int id) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
log.error(e);
}
ProdottoVO prodotto = null;
switch (id) {
case 1:
prodotto = new ProdottoVO(1, "prodotto 1", "xxxxx");
break;
case 2:
prodotto = new ProdottoVO(2, "prodotto 2", "yyyyyy");
break;
case 3:
prodotto = new ProdottoVO(3, "prodotto 3", "zzzzz");
break;
default:
prodotto = null;
break;
}
return prodotto;
}
}
La classe ProdottoManager contiene due metodi principali:
- public ProdottoVO get(int id)
cache.get("id_" + id)
cache.put("id_" + id, prodotto)
- private ProdottoVO load(int id)
Per notare come la cache riesca ad ottimizzare il tempo di caricamento è necessario richiamare più volte il metodo get
della classe ProdottoManager. La prima esecuzione risulterà sicuramente più lenta perché gli elementi richiesti non sono presenti nella cache, ma le successive esecuzioni risulteranno molto più veloci.
Listato 2. Copia il contenuto della cache sul disco
public class CacheTest{
public static void main(String[] args){
long timeStart = System.currentTimeMillis();
ProdottoManager pm = ProdottoManager.getInstance();
ProdottoVO p1 = pm.get(1);
ProdottoVO p2 = pm.get(2);
ProdottoVO p3 = pm.get(3);
pm.getCache().dispose();
long timeStop = System.currentTimeMillis();
long timer = timeStop - timeStart;
System.out.println(timeStart + " " + timeStop + " " + timer);
System.exit(0);
}
}
Il metodo dispose()
permette di copiare il contenuto della cache sul disco. In questo modo dalla seconda esecuzione in poi, la cache verrà inizializzata con i prodotti presenti sul disco.