Desarrollar un servicio de almacenamiento en caché genérico para mejorar el rendimiento

Suponga que un compañero de trabajo le pide una lista de todos los países del mundo. Como no es un experto en geografía, navega hasta el sitio web de las Naciones Unidas, descarga la lista y se la imprime. Sin embargo, solo desea examinar la lista; en realidad no se lo lleva. Como lo último que necesita es otra hoja de papel en su escritorio, alimenta la lista a la trituradora.

Un día después, otro compañero de trabajo solicita lo mismo: una lista de todos los países del mundo. Maldiciéndose por no guardar la lista, vuelve a navegar de nuevo al sitio web de las Naciones Unidas. En esta visita al sitio web, observa que la ONU actualiza su lista de países cada seis meses. Descarga e imprime la lista para su compañero de trabajo. Él lo mira, le da las gracias y, de nuevo, le deja la lista. Esta vez archiva la lista con un mensaje en una nota adhesiva adjunta que le recuerda que debe descartarla después de seis meses.

Efectivamente, durante las próximas semanas sus compañeros de trabajo continuarán solicitando la lista una y otra vez. Se felicita por haber archivado el documento, ya que puede extraerlo del archivador más rápidamente de lo que puede extraerlo del sitio web. Su concepto de archivador se pone de moda; pronto todos comienzan a poner artículos en su gabinete. Para evitar que el gabinete se desorganice, establezca las pautas para su uso. En su calidad oficial como administrador del archivador, instruye a sus compañeros de trabajo a colocar etiquetas y notas adhesivas en todos los documentos, que identifican los documentos y su fecha de descarte / vencimiento. Las etiquetas ayudan a sus compañeros de trabajo a ubicar el documento que están buscando y las notas adhesivas califican si la información está actualizada.

El archivador se vuelve tan popular que pronto no podrá archivar ningún documento nuevo en él. Debe decidir qué desechar y qué conservar. Aunque deseche todos los documentos caducados, el gabinete aún se desborda de papel. ¿Cómo decide qué documentos no vencidos desechar? ¿Descarta el documento más antiguo? Puede descartar los que se utilizan con menos frecuencia o los que se utilizan menos recientemente; en ambos casos, necesitará un registro que indique cuándo se accedió a cada documento. O quizás podría decidir qué documentos descartar basándose en algún otro determinante; la decisión es puramente personal.

Para relacionar la analogía del mundo real anterior con el mundo de las computadoras, el archivador funciona como un caché: una memoria de alta velocidad que ocasionalmente necesita mantenimiento. Los documentos en la caché son objetos almacenados en caché, todos los cuales cumplen con los estándares establecidos por usted, el administrador de caché. El proceso de limpieza de la caché se denomina purga. Debido a que los elementos almacenados en caché se purgan después de que haya transcurrido una cierta cantidad de tiempo, el caché se denomina caché temporizado.

En este artículo, aprenderá a crear una caché Java 100 por ciento pura que utiliza un hilo de fondo anónimo para purgar elementos caducados. Verá cómo diseñar un caché de este tipo mientras comprende las compensaciones involucradas con varios diseños.

Construye el caché

Basta de analogías con los archivadores: pasemos a los sitios web. Los servidores de sitios web también deben lidiar con el almacenamiento en caché. Los servidores reciben repetidamente solicitudes de información, que son idénticas a otras solicitudes. Para su próxima tarea, debe crear una aplicación de Internet para una de las empresas más grandes del mundo. Después de cuatro meses de desarrollo, incluidas muchas noches de insomnio y demasiadas colas Jolt, la aplicación pasa a pruebas de desarrollo con 1.000 usuarios. Una prueba de certificación de 5.000 usuarios y un posterior despliegue de producción de 20.000 usuarios sigue a las pruebas de desarrollo. Sin embargo, después de recibir errores de falta de memoria mientras solo 200 usuarios prueban la aplicación, las pruebas de desarrollo se detienen.

Para discernir el origen de la degradación del rendimiento, utiliza un producto de creación de perfiles y descubre que el servidor carga varias copias de bases de datos ResultSet, cada una de las cuales tiene varios miles de registros. Los registros componen una lista de productos. Además, la lista de productos es idéntica para todos los usuarios. La lista no depende del usuario, como podría haber sido el caso si la lista de productos hubiera resultado de una consulta parametrizada. Decide rápidamente que una copia de la lista podría servir a todos los usuarios simultáneos, por lo que la almacena en caché.

Sin embargo, surgen varias preguntas, que incluyen complejidades tales como:

  • ¿Y si cambia la lista de productos? ¿Cómo puede la caché caducar las listas? ¿Cómo sabré cuánto tiempo debe permanecer la lista de productos en la caché antes de que caduque?
  • ¿Qué pasa si existen dos listas de productos distintas y las dos listas cambian a intervalos diferentes? ¿Puedo caducar cada lista individualmente o todas deben tener la misma vida útil?
  • ¿Qué pasa si la caché está vacía y dos solicitantes la prueban exactamente al mismo tiempo? Cuando ambos lo encuentren vacío, ¿crearán sus propias listas y luego ambos intentarán poner sus copias en el caché?
  • ¿Qué pasa si los elementos permanecen en el caché durante meses sin que se acceda a ellos? ¿No se comerán la memoria?

Para abordar estos desafíos, debe crear un servicio de almacenamiento en caché de software.

En la analogía del archivador, las personas siempre revisaban primero el archivador cuando buscaban documentos. Su software debe implementar el mismo procedimiento: una solicitud debe verificar el servicio de almacenamiento en caché antes de cargar una lista nueva de la base de datos. Como desarrollador de software, su responsabilidad es acceder a la caché antes de acceder a la base de datos. Si la lista de productos ya se cargó en la caché, entonces usa la lista en caché, siempre que no esté vencida. Si la lista de productos no está en la caché, cárguela desde la base de datos y la caché inmediatamente.

Nota: Antes de continuar con los requisitos y el código del servicio de almacenamiento en caché, es posible que desee consultar la barra lateral a continuación, "Almacenamiento en caché versus agrupación". Explica la agrupación, un concepto relacionado.

Requisitos

De acuerdo con los buenos principios de diseño, definí una lista de requisitos para el servicio de almacenamiento en caché que desarrollaremos en este artículo:

  1. Cualquier aplicación Java puede acceder al servicio de almacenamiento en caché.
  2. Los objetos se pueden colocar en la caché.
  3. Los objetos se pueden extraer de la caché.
  4. Los objetos almacenados en caché pueden determinar por sí mismos cuándo caducan, lo que permite la máxima flexibilidad. Los servicios de almacenamiento en caché que caducan todos los objetos con la misma fórmula de caducidad no proporcionan un uso óptimo de los objetos almacenados en caché. Este enfoque es inadecuado en sistemas a gran escala ya que, por ejemplo, una lista de productos puede cambiar a diario, mientras que una lista de ubicaciones de tiendas puede cambiar solo una vez al mes.
  5. Un subproceso en segundo plano que se ejecuta con baja prioridad elimina los objetos caducados en caché.
  6. El servicio de almacenamiento en caché se puede mejorar posteriormente mediante el uso de un mecanismo de purga de uso menos reciente (LRU) o de uso menos frecuente (LFU).

Implementación

Para satisfacer el Requisito 1, adoptamos un entorno Java 100% puro. Al proporcionar métodos públicos gety seten el servicio de almacenamiento en caché, también cumplimos con los requisitos 2 y 3.

Antes de continuar con la discusión del Requisito 4, mencionaré brevemente que satisfaceremos el Requisito 5 creando un hilo anónimo en el administrador de caché; este hilo comienza en el bloque estático. Además, cumplimos con el Requisito 6 al identificar los puntos en los que se agregaría código posteriormente para implementar los algoritmos LRU y LFU. Entraré en más detalles sobre estos requisitos más adelante en el artículo.

Ahora, volvamos al Requisito 4, donde las cosas se vuelven interesantes. Si cada objeto almacenado en caché debe determinar por sí mismo si ha expirado, entonces debe tener una forma de preguntarle al objeto si ha expirado. Eso significa que los objetos en la caché deben cumplir con ciertas reglas; lo logras en Java implementando una interfaz.

Comencemos con las reglas que gobiernan los objetos colocados en la caché.

  1. Todos los objetos deben tener un método público llamado isExpired(), que devuelve un valor booleano.
  2. Todos los objetos deben tener un método público llamado getIdentifier(), que devuelve un objeto que lo distingue de todos los demás en la caché.

Note: Before jumping straight into the code, you must understand that you can implement a cache in many ways. I have found more than a dozen different implementations. Enhydra and Caucho provide excellent resources that contain several cache implementations.

You'll find the interface code for this article's caching service in Listing 1.

Listing 1. Cacheable.java

/** * Title: Caching Description: This interface defines the methods, which must be implemented by all objects wishing to be placed in the cache. * * Copyright: Copyright (c) 2001 * Company: JavaWorld * FileName: Cacheable.java @author Jonathan Lurie @version 1.0 */ public interface Cacheable { /* By requiring all objects to determine their own expirations, the algorithm is abstracted from the caching service, thereby providing maximum flexibility since each object can adopt a different expiration strategy. */ public boolean isExpired(); /* This method will ensure that the caching service is not responsible for uniquely identifying objects placed in the cache. */ public Object getIdentifier(); } 

Any object placed in the cache -- a String, for example -- must be wrapped inside an object that implements the Cacheable interface. Listing 2 is an example of a generic wrapper class called CachedObject; it can contain any object needed to be placed in the caching service. Note that this wrapper class implements the Cacheable interface defined in Listing 1.

Listing 2. CachedManagerTestProgram.java

/** * Title: Caching * Description: A Generic Cache Object wrapper. Implements the Cacheable interface * uses a TimeToLive stategy for CacheObject expiration. * Copyright: Copyright (c) 2001 * Company: JavaWorld * Filename: CacheManagerTestProgram.java * @author Jonathan Lurie * @version 1.0 */ public class CachedObject implements Cacheable { // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ /* This variable will be used to determine if the object is expired. */ private java.util.Date dateofExpiration = null; private Object identifier = null; /* This contains the real "value". This is the object which needs to be shared. */ public Object object = null; // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ public CachedObject(Object obj, Object id, int minutesToLive) { this.object = obj; this.identifier = id; // minutesToLive of 0 means it lives on indefinitely. if (minutesToLive != 0) { dateofExpiration = new java.util.Date(); java.util.Calendar cal = java.util.Calendar.getInstance(); cal.setTime(dateofExpiration); cal.add(cal.MINUTE, minutesToLive); dateofExpiration = cal.getTime(); } } // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ public boolean isExpired() { // Remember if the minutes to live is zero then it lives forever! if (dateofExpiration != null) { // date of expiration is compared. if (dateofExpiration.before(new java.util.Date())) { System.out.println("CachedResultSet.isExpired: Expired from Cache! EXPIRE TIME: " + dateofExpiration.toString() + " CURRENT TIME: " + (new java.util.Date()).toString()); return true; } else { System.out.println("CachedResultSet.isExpired: Expired not from Cache!"); return false; } } else // This means it lives forever! return false; } // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ public Object getIdentifier() { return identifier; } // +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ } 

The CachedObject class exposes a constructor method that takes three parameters:

public CachedObject(Object obj, Object id, int minutesToLive) 

The table below describes those parameters.

Parameter descriptions of the CachedObject constructor
Name Type Description
Obj Object The object that is shared. It is defined as an object to allow maximum flexibility.
Id Object Id contains a unique identifier that distinguishes the obj parameter from all other objects residing in the cache. The caching service is not responsible for ensuring the uniqueness of the objects in the cache.
minutesToLive Int The number of minutes that the obj parameter is valid in the cache. In this implementation, the caching service interprets a value of zero to mean that the object never expires. You might want to change this parameter in the event that you need to expire objects in less than one minute.

The constructor method determines the expiration date of the object in the cache using a time-to-live strategy. As its name implies, time-to-live means that a certain object has a fixed time at the conclusion of which it is considered dead. By adding minutesToLive, the constructor's int parameter, to the current time, an expiration date is calculated. This expiration is assigned to the class variable dateofExpiration.

Ahora, el isExpired()método simplemente debe determinar si dateofExpirationes anterior o posterior a la fecha y hora actuales. Si la fecha es anterior a la hora actual y el objeto almacenado en caché se considera caducado, el isExpired()método devuelve verdadero; si la fecha es posterior a la hora actual, el objeto almacenado en caché no caduca y isExpired()devuelve falso. Por supuesto, si dateofExpirationes nulo, que sería el caso si minutesToLivefuera cero, entonces el isExpired()método siempre devuelve falso, lo que indica que el objeto en caché vive para siempre.