Caché en Aplicaciones Cliente/Servidor

El caché es una de las primera técnicas usadas para aumetnar el rendimiento de navegadores y servidores web. El caché del navegador hace innecesarios los bloqueos de red porque una copia reciente del fichero se mantiene en el caché local, y el caché del servidor reduce el coste de la carga de ficheros desde disco para cada petición. Esta sección explica cómo podes usar el caché de forma similar para mejorar el rendimiento en muchas aplicaciones cliente/servidor escritas en lenguaje JavaTM.

El API java.util.Collections disponible en el SDK Java® 2 hace sencilla la implementación del caché. Este API proporciona la clase HashMap, que funciona bien para cachear un objeto, y la clase LinkedList, que funciona bien en combinaciones con la clase HashMap para cachear muchos objetos.


Caché de un Objeto

Un objeto HashMap almacena objetos en una pareja clave valor. cuando ponemos un datp en un HashMap, le asignamos una clave y luego usamos esa clave para recuperar el dato.

Un objeto HashMap es muy similar a un Hashtable y puede ser usado para mantener una copia temporal de resultados generados préviamente. Los objetos mantenidos en el caché HashMap podría, por ejemplo, ser una lista de subastas completadas.

En este caso, los resultados de una consulta JDBC podrían solicitarse cientos de veces en un segundo por personas que están esperando conocer la puja más alta, pero la lista de resultados completa sólo cambia una vez por minuto cuando se ompleta una subasta. Podemos escribir nuestro programa para recuperar los objetos que no han cambiado desde el caché de resultados en vez de solicitar a la base de datos cada vez y obtener un significante aumento de rendimiento.

Este ejemplo de código ejecuta una consulta a la base de datos por cada minuto, y devuelve copias cacheadas para las solicitudes que vienen entre consultas.

import java.util.*;
import java.io.*;

class DBCacheRecord {
  Object data;
  long time;

  public DBCacheRecord(Object results, long when) {
    time=when;
    data=results;
  }
  public Object getResults() {
	return data;
  }
  public long getLastModified() {
	return time;
  }
}

public class DBCache {
  Map cache;

  public DBCache() {
    cache = new HashMap();
  }

  public Object getDBData(String dbcommand) {
    if(!cache.containsKey(dbcommand)) {
	synchronized(cache) {	
	  cache.put(dbcommand, readDBData(dbcommand)); 
        }
     } else {
       if((new Date().getTime() ) -
         ((DBCacheRecord)cache.get(
		dbcommand)).getLastModified()>=1000) {
	 synchronized(cache) {	
	   cache.put(dbcommand, readDBData(dbcommand)); 
         }
       }	
     }
     return ((DBCacheRecord)cache.get(
	dbcommand)).getResults();
  }

  public Object readDBData(String dbcommand) {

/*Insert your JDBC code here For Example:
  ResultSet results=stmt.executeQuery(dbcommand);
*/
    String results="example results";
    return(new DBCacheRecord(results,new 
			     Date().getTime()));
	
  }

  public static void main(String args[]) {
    DBCache d1=new DBCache();
    for(int i=1;i<=20;i++) {
	d1.getDBData(
	  "select count(*) from results where 
	  TO_DATE(results.completed) <=SYSDATE");
    }
  }
}

Cache de Muchos Objetos

Algunas veces queremos cachear más de un objeto. Por ejemplo, podríamos querer mantener los ficheros accedidos más recientemente en el caché de un servidor web. Si usamos un objeto HashMap para un propósito como este, continuará creciendo y usando mucha memoria.

Si nuestra máquina tiene una gran cantidad de memoria y sólo un pequeño número de objetos que cachear entonces un creciente HashMap podría no ser un problema. Sin embargo, si estamos intentar cachear muchos objetos entonces podríamos queres sólo mantener los objetos más recientes en el caché proporcionando el mejor uso de la mémoria de la máquina. Podemos combinar un objeto HashMap con un LinkedList para crear un caché llamado "Most Recently Used" (MRU).

Con un caché MRU, podemos situar una restricción sobre los objetos que permanecen en el caché, y por lo tanto, control sobre el tamaño del caché. Hay tres operaciones principales que puede realizar un caché MRU:

Este diagrama muestra cómo trabajan juntos LinkedList y HashMap para implementar las operaciones descritas arriba.

Caché MRU con LinkedList y HashMap

El LinkedList proporciona el mecanismo de cola, y las entradas de la LinkedList contienen la clave de los datos en el HashMap. Para añadir una nueva entrada en la parte superior de la lista, se llama al método addFirst.

El API Collectios no implementa bloqueos, por eso si eliminados o añadimos entradas a objetos LinkedList o HashMap, necesitamos bloquear los accesos a esos objetos. También podemos usar un Vector o ArrayList para obtener el mismo resultado mostrado en el códido de abajo del LinkedList.

Este ejemplo de código usa un caché MRU para mantener un caché de ficheros cargados desde disco. Cuando se solicita un fichero, el programa chequea para ver si el fichero está en el caché. Si el fichero no está en el caché, el programa lee el fichero desde el disco y sitúa una copia en el caché al principio de la lista.

Si el fichero está en el caché, el programa compara la fecha de modificación del fichero y la entrada del caché.

import java.util.*;
import java.io.*;

class myFile {
  long lastmodified;
  String contents;

  public myFile(long last, String data) {
    lastmodified=last;
    contents=data;		
  }
  public long getLastModified() {
    return lastmodified;
  }
  public String getContents() {
    return contents;
  }
}

public class MRUCache {

  Map cache;
  LinkedList mrulist;
  int cachesize;

  public MRUCache(int max) {
    cache = new HashMap();
    mrulist= new LinkedList();
    cachesize=max;
  }

  public String getFile(String fname) {
    if(!cache.containsKey(fname)) {
      synchronized(cache) {	
        if(mrulist.size() >=cachesize) {
	  cache.remove(mrulist.getLast());
	  mrulist.removeLast();
        }
        cache.put(fname, readFile(fname)); 
        mrulist.addFirst(fname);
      }
    } else {
      if((new File(fname).lastModified())> 
	((myFile)cache.get(fname)).getLastModified()) {
	  synchronized(cache) {	
	    cache.put(fname, readFile(fname)); 
          }
       }	
       synchronized(cache) {
	 mrulist.remove(fname);
	 mrulist.addFirst(fname);
       }
    }
       return ((myFile)cache.get(fname)).getContents();
  }
	
  public myFile readFile(String name) {
    File f = new File(name);
    StringBuffer filecontents= new StringBuffer();

    try {
      BufferedReader br=new BufferedReader(
                              new FileReader(f));
      String line;

      while((line =br.readLine()) != null) {
	filecontents.append(line);
      }	
    } catch (FileNotFoundException fnfe){
      return (null);
    } catch ( IOException ioe) {
		return (null);
    }
      return (new myFile(f.lastModified(), 
		filecontents.toString()));
  }

  public void printList() {
    for(int i=0;i<mrulist.size();i++) {
      System.out.println("item "+i+"="+mrulist.get(i));
    }
  }

  public static void main(String args[]) {

    // Number of entries in MRU cache is set to 10 
    MRUCache h1=new MRUCache(10);
    for(int i=1;i<=20;i++) {
      // files are stored in a subdirectory called data
      h1.getFile("data"+File.separatorChar+i);
    }
      h1.printList();
  }
}

Ozito