Autor: Iqbal Kan
El almacenamiento en caché mejora en gran medida el rendimiento de la aplicación porque reduce los costosos viajes a la base de datos. Pero, si desea utilizar el almacenamiento en caché en su aplicación, debe decidir qué almacenar en caché y dónde colocar su código de almacenamiento en caché. La respuesta es simple. Guarde en caché los objetos de su dominio y coloque el código de almacenamiento en caché dentro de sus clases de persistencia. Los objetos de dominio son fundamentales para cualquier aplicación y representan sus datos principales y las reglas de validación comercial. Y, aunque los objetos de dominio pueden conservar algunos datos de solo lectura, la mayoría de los datos son transaccionales y cambian con frecuencia. Por lo tanto, no puede simplemente mantener los objetos de dominio como "variables globales" para la totalidad de su aplicación porque los datos cambiarán en la base de datos y sus objetos de dominio se volverán obsoletos, lo que causará problemas de integridad de datos. Tendrá que usar una solución de almacenamiento en caché adecuada para esto. Y sus opciones son ASP.NET Cache, Caching Application Block en Microsoft Enterprise Library o alguna solución comercial como NCache en Alachisoft . Personalmente, desaconsejaría el uso de ASP.NET Cache, ya que lo obliga a almacenar en caché desde la capa de presentación (páginas ASP.NET), lo cual es malo. El mejor lugar para incrustar el almacenamiento en caché en su aplicación son las clases de persistencia de objetos de su dominio. En este artículo, extiendo un patrón de diseño anterior que escribí llamado Patrón de persistencia de objetos de dominio para .NET. Le mostraré cómo puede incorporar el almacenamiento en caché inteligente en su aplicación para aumentar su rendimiento y qué consideraciones debe tener en cuenta al hacerlo.
El patrón de almacenamiento en caché de objetos de dominio intenta proporcionar una solución para el almacenamiento en caché de objetos de dominio. Los objetos de dominio en este patrón desconocen las clases que los conservan o si se almacenan en caché o no, porque la dependencia es solo unidireccional. Esto hace que el diseño del objeto de dominio sea mucho más simple y fácil de entender. También oculta el código de persistencia y almacenamiento en caché de otros subsistemas que utilizan los objetos de dominio. Esto también funciona en sistemas distribuidos donde solo se pasan los objetos de dominio.
Objetos de dominio, almacenamiento en caché de objetos de dominio.
Los objetos de dominio forman la columna vertebral de cualquier aplicación. Capturan el modelo de datos de la base de datos y también las reglas comerciales que se aplican a estos datos. Es muy típico que la mayoría de los subsistemas de una aplicación se basen en estos objetos de dominio común. Y, por lo general, las aplicaciones pasan la mayor parte de su tiempo cargando o guardando estos objetos de dominio en la base de datos. El "tiempo de procesamiento" real de estos objetos es muy pequeño, especialmente para aplicaciones N-Tier donde cada "solicitud de usuario" es muy breve.
Esto significa que el rendimiento de la aplicación depende en gran medida de la rapidez con que estos objetos de dominio estén disponibles para la aplicación. Si la aplicación tiene que realizar numerosos viajes a la base de datos, el rendimiento suele ser malo. Pero, si la aplicación almacena en caché estos objetos cerca, el rendimiento mejora considerablemente.
Al mismo tiempo, es muy importante que el código de almacenamiento en caché de objetos de dominio se mantenga en un lugar tan central que no importa quién cargue o guarde los objetos de dominio, la aplicación interactúa automáticamente con el caché. Además, debemos ocultar el código de almacenamiento en caché del resto de la aplicación para que podamos sacarlo fácilmente si es necesario.
Como se describió anteriormente, la solución es una extensión de un patrón de diseño existente denominado Patrón de persistencia de objetos de dominio para .NET. Ese patrón ya logra el objetivo de separar los objetos de dominio del código de persistencia y también del resto de la aplicación. Este doble desacoplamiento proporciona una gran flexibilidad en el diseño. Los objetos de dominio y el resto de la aplicación no se ven afectados en absoluto, ya sea que los datos provengan de una base de datos relacional o de cualquier otra fuente (por ejemplo, XML, archivos planos o Active Directory/LDAP).
Por lo tanto, el mejor lugar para incrustar el código de almacenamiento en caché es en las clases de persistencia. Esto garantiza que, independientemente de la parte de la aplicación que emita la llamada de carga o de guardado a los objetos de dominio, primero se haga la referencia adecuada al almacenamiento en caché. Esto también oculta todo el código de almacenamiento en caché del resto de la aplicación y le permite reemplazarlo con algo más si decide hacerlo.
En este ejemplo, veremos una clase de empleado de la base de datos Northwind asignada a la tabla "Empleados" en la base de datos.
// Domain object "Employee" that holds your data
public class Employee {
// Some of the private data members
// ...
public Employee () { }
// Properties for Employee object
public String EmployeeId {
get { return _employeeId; }
set { _employeeId = value; }
}
public String Title {
get { return _title; }
set { _title = value; }
}
public ArrayList Subordinates {
get { return _subordinates; }
set { _subordinates = value; }
}
}
// Interface for the Employee persistence
public interface IEmployeeFactory {
// Standard transactional methods for single-row operations
void Load (Employee emp);
void Insert (Employee emp);
void Update (Employee emp);
void Delete (Employee emp);
// Load the related Employees (Subordinates) for this Employee
void LoadSubordinates (Employee emp);
// Query method to return a collection of Employee objects
ArrayList FindByTitle (String title);
}
// Implementation of Employee persistence
public class EmployeeFactory : IEmployeeFactory {
// all methods described in interface above are implemented here
}
// A FactoryProvider to hide persistence implementation
public class FactoryProvider {
// To abstract away the actual factory implementation
public static IEmployeeFactory GetEmployeeFactory () { return new EmployeeFactory (); }
}
A continuación se muestra un ejemplo de cómo una aplicación cliente utilizará este código.
public class NorthwindApp
{
static void Main (string[] args) {
Employee emp = new Employee();
IEmployeeFactory iEmpFactory = FactoryProvider.GetEmployeeFactory();
// Let's load an employee from Northwind database.
emp.EmployeeId = 2;
iEmpFactory.load(emp);
// Pass on the Employee object
HandleEmployee(emp);
HandleSubordinates(emp.Subordinates);
// empList is a collection of Employee objects
ArrayList empList = iEmpFactory.FindByTitle("Manager");
}
}
El código anterior le muestra la estructura general de sus clases para manejar la persistencia y el almacenamiento en caché de los objetos de dominio. Como puede ver, existe una separación clara entre el dominio y las clases de persistencia. Y hay una clase FactoryProvider adicional que le permite ocultar la implementación de persistencia del resto de la aplicación. Sin embargo, los objetos de dominio (Empleado en este caso) se mueven por toda la aplicación.
La mayoría de los sistemas de caché le proporcionan una clave basada en cadenas. Al mismo tiempo, los datos que almacena en caché se componen de varias clases diferentes ("Clientes", "Empleados", "Pedidos", etc.). En esta situación, un EmployeeId de 1000 puede entrar en conflicto con un OrderId de 1000 si sus claves no contienen ningún tipo de información. Por lo tanto, también debe almacenar cierta información de tipo como parte de la clave. A continuación se sugieren algunas estructuras clave. Puedes inventarte los tuyos basados en los mismos principios.
La mayoría de los datos transaccionales contienen operaciones de una sola fila (cargar, insertar, actualizar y eliminar). Todos estos métodos se basan en valores de clave principal del objeto y son el lugar ideal para comenzar a colocar el código de almacenamiento en caché. Aquí está cómo manejar cada método:
A continuación se muestra un método de carga de muestra con lógica de almacenamiento en caché incluida. Recuerde, solo está cargando un solo objeto (una sola fila) de la base de datos.
// Check the cache before going to the database
void Load(Employee emp)
{
try{
// Construct a cache-key to lookup in the cache first
// The cache-key for the object will be like this: Employees:PK:1000stringobjKey = CacheUtil.GetObjectKey("Employee", emp.EmployeeId.ToString());
objectobj = CacheUtil.Load(objKey);
if(obj == null)
{
// item not found in the cache. Load from database and then store in the cache_LoadFromDb(emp);
// For simplicity, let's assume this object does not depend on anything elseArrayListdependencyKeys = null;
CacheItemRemovedCallbackonRemoved = null;
CacheUtil.Store(objKey, emp, dependencyKeys, Cache.NoAbsoluteExpiration,
Cache.NoSlidingExpiration, CacheItemPriority.Default, onRemoved );
// Now, load all its related subordinatesLoadSubordinates(emp);
}
else {
emp.Copy((Employee)obj);
}
}
catch(Exception e)
{
// Handle exceptions here
}
}
Tenga en cuenta algunas cosas aquí.
Los objetos de dominio generalmente representan datos relacionales provenientes de una base de datos relacional. Por lo tanto, cuando los almacene en caché, debe tener en cuenta sus relaciones y también almacenar en caché los objetos relacionados. Y también debe crear una "dependencia" entre el objeto y todos sus objetos relacionados. La razón es que si elimina el objeto del caché, también debe eliminar todos sus objetos relacionados para que no haya problemas de integridad de datos. A continuación se muestra un ejemplo de código de cómo especificar relaciones en la memoria caché.
// LoadSubordinates method
void LoadSubordinates (Employee emp) {
try {
// Construct a cache-key to lookup related items in the cache first
// The cache-key for related collection will be like
this : Employees : PK : 1000 : REL : Subordinates
string relKey = CacheUtil.GetRelationKey ("Employees",
"Subordinates", emp.EmployeeId.ToString ());
string employeeKey = CacheUtil.GetObjectKey ("Employee",
emp.EmployeeId.ToString ());
object obj = CacheUtil.Load (relKey);
if (obj == null) {
// Subordinates not found in the cache. Load from
database and then store in the cache
_LoadSubordinatesFromDb (emp);
ArrayList subordList = emp.Subordinates;
// Result is a collection of Employee. Let's store
each Employee separately in
// the cache and then store the collection also but
with a dependency on all the
// individual Employee objects. Then, if any Employee
is removed, the collection will also be
// Count + 1 is so we can also put a dependency on
the Supervisor
ArrayList dependencyKeys = new ArrayList (subordList.Count + 1);
for (int index = 0; index, subordList.Count; index++) {
string objKey = CacheUtil.GetObjectKey ("Employee",
subordList[index].EmployeeId.ToString ());
CacheUtil.Store (objKey, subordList[index], null,
Cache.NoAbsoluteExpiration,
Cache.NoSlidingExpiration, CacheItemPriority.Default, null);
dependencyKeys[index] = objKey;
}
dependencyKeys[subordList.Count] = employeeKey;
CacheItemRemovedCallback onRemoved = null;
CacheUtil.Store (relKey, subordinateList,
dependencyKeys, Cache.NoAbsoluteExpiration,
Cache.NoSlidingExpiration, CacheItemPriority.Default,
onRemoved);
} else {
// Subordinates already in the cache. Let's get them
emp.Subordinates = (ArrayList) obj;
}
}
catch (Exception e) {
// Handle exceptions here
}
}
En el ejemplo anterior, notará que se devuelve una colección desde la base de datos y cada objeto dentro de la colección se almacena individualmente en el caché. Luego, la colección se almacena en caché como un elemento único pero con una dependencia de caché en todos los objetos individuales de la colección. Esto significa que si alguno de los objetos individuales se actualiza o elimina en la memoria caché, la memoria caché elimina automáticamente la colección. Esto le permite mantener la integridad de los datos en las colecciones de almacenamiento en caché.
También notará en el ejemplo anterior que la colección tiene una dependencia de caché en el "objeto principal" cuyos objetos relacionados contiene la colección. Esta dependencia también significa que si el objeto principal se elimina o actualiza en la memoria caché, la colección se eliminará para mantener la integridad de los datos.
Un método de consulta devuelve una colección de objetos en función de los criterios de búsqueda especificados en él. Puede o no tomar ningún parámetro de tiempo de ejecución. En nuestro ejemplo, tenemos un FindByTitle que toma "título" como parámetro. A continuación se muestra un ejemplo de cómo se incrusta el almacenamiento en caché dentro de un método de consulta.
// Query method to return a collection
ArrayList FindByTitle (String title) {
try {
// Construct a cache-key to lookup items in the cache first
// The cache-key for the query will be like this:
Employees : PK : 1000 : QRY : FindByTitle : Manager
string queryKey = CacheUtil.GetQueryKey ("Employees",
"Query", title);
object obj = CacheUtil.Load (queryKey);
if (obj == null) {
// No items found in the cache. Load from database
and then store in the cache
ArrayList empList = _FindByTitleFromDb (title);
// Result is a collection of Employee. Let's store
each Employee separately in
// the cache and then store the collection also
but with a dependency on all the
// individual Employee objects. Then, if any Employee
is removed, the collection will also be
ArrayList dependencyKeys = new ArrayList (empList.Count);
for (int index = 0; index, empList.Count; index++) {
string objKey = CacheUtil.GetObjectKey ("Employee",
empList[index].EmployeeId.ToString ());
CacheUtil.Store (objKey, empList[index], null, Cache.NoAbsoluteExpiration,
Cache.NoSlidingExpiration, CacheItemPriority.Default, null);
dependencyKeys[index] = objKey;
}
CacheItemRemovedCallback onRemoved = null;
CacheUtil.Store (queryKey, empList, dependencyKeys,
Cache.NoAbsoluteExpiration,
Cache.NoSlidingExpiration, CacheItemPriority.Default, onRemoved);
} else {
// Query results already in the cache. Let's get them
return (ArrayList) obj;
}
}
catch (Exception e) {
// Handle exceptions here
}
}
En el ejemplo anterior, al igual que el método de relación, notará que la base de datos devuelve una colección y cada objeto dentro de la colección se almacena individualmente en la memoria caché. Luego, la colección se almacena en caché como un elemento único pero con una dependencia de caché en todos los objetos individuales de la colección. Esto significa que si alguno de los objetos individuales se actualiza o elimina en la memoria caché, la memoria caché elimina automáticamente la colección. Esto le permite mantener la integridad de los datos en las colecciones de almacenamiento en caché.
El patrón anterior funciona tanto para entornos de implementación de un solo servidor como de granja de servidores. Lo único que debe cambiar es la solución de almacenamiento en caché subyacente. La mayoría de las soluciones de almacenamiento en caché son para entornos de un solo servidor (por ejemplo, ASP.NET Cache y Caching Application Block). Pero, hay algunas soluciones comerciales como Alachisoft NCache (http: // www.alachisoft.com) que le proporcionan una caché distribuida que funciona en una configuración de granja de servidores. De esta manera, su aplicación puede usar un caché de cualquier servidor en la granja y todas las actualizaciones de caché se propagan inmediatamente a toda la granja de servidores.
Usando el patrón de almacenamiento en caché de objetos de dominio, hemos demostrado cómo debe incrustar el código de almacenamiento en caché en sus clases de persistencia. Y hemos cubierto las situaciones más utilizadas de carga, consultas y relaciones con respecto al almacenamiento en caché. Esto debería brindarle un buen punto de partida para determinar cómo debe usar el almacenamiento en caché en su aplicación.
Escrito por: Iqbal Khan trabaja para Alachisoft, una empresa de software líder que ofrece soluciones de almacenamiento en caché distribuido .NET y Java, mapeo O/R y optimización de almacenamiento de SharePoint. Puedes localizarlo en iqbal@alachisoft.com.