Simple reducción de Materializaciones en Instancias de Lectura

Investigando y desarrollando soluciones de objetos basadas en mod_perl, se encuentran situaciones recurrentes que en términos modernos denominamos patrones. A continuación les detallaré uno de los patrones que utilizo de manera recurrente, para reducir las Materializaciones de objetos en instancias de lectura.

Definiciones Básicas

Materialización

Inicializar una instancia de un objeto a partir de información proveniente de un backstore, en este caso una base de datos relacional.

Instanciación

Obtener una instancia o referencia a un objeto.

backstore

Almacenamiento de la información necesaria para inicializar los objetos.

El problema

Durante la ejecución de un modelo basado en objetos que usan un backstore relacional, se hace de suma importancia optimizar (reducir) la materialización para evitar accesos innecesarios a la base de datos.

Normalmente para instanciar un objeto se recupera la información de la tabla relacional que le sirve como backstore usando la llave primaria.

Cada vez que llega un requerimiento instanciamos todos los objetos que se necesitan para satisfacerlo, es decir leemos las tablas de la base de datos que contienen el backstore de cada objeto que instanciemos.

De este modo en cada requerimiento el modelo de objetos activo refleja la información almacenada en la base de datos desde la última actualización (commit).

Llamaremos a esto "materialización on request", es decir los objetos son materializados fresquitos a partir de información en la base de datos en cada request.

Esta forma de operación resuelve un nivel de concurrencia aceptable, debido a que la actualización y lectura funcionarían así:

1.- El requerimiento A actualiza un objeto, y por lo tanto se actualiza la fila de la tabla en la base de datos que es el backstore para él.
2.- Llega el requerimiento B.
3.- El requerimiento B instancia el mismo objeto, que al ser materializado desde la base de datos ve en forma automática los datos que fueron actualizados por requerimiento A.




El problema de lo anterior es que el se realizan decenas de materializaciones con cada petición, que en algunos casos podrían evitarse.

Nuestro problema es reducir el número de accesos a la base de datos. Lo haremos creando un cache para objetos que son principalmente de lectura, el siguiente es el código normal de instanciación que usa materialización on request:

El siguiente es el código que uso normalmente para instanciación con materialización on request:
package Comuna

sub new {

my ($class, $id_comuna) = @_;

return undef unless $id_comuna =~ /^\d+$/;

my $sth = $dbh->prepare("select * from comuna where id_comuna = $id_comuna");
$sth->execute;
if (my $self = $sth->fetchrow_hashref) {
return bless $self, $class;
} else {
return undef;
}

}

# Al pedir una instancia lo hacemos de la siguiente forma:

if (my $c = new Comuna($id_comuna)) {
print $c->nombre, "\n";
} else {
print "[No se pudo recuperar la comuna]";
}
...
Notas:Como es clásico, los campos de la tabla pasan a ser las variables de instancia del objeto.
Hay formas sencillas para crear metodos accesores y mutadores usando AutoLoad, pero en general no uso este sistema y creo los accesores manualmente a medida que los voy necesitando.

Solución

Existen objetos cuyas instancias son de lectura, podemos crear un cache que se puede poblar en forma dinámica y retornar instancias ya prematerializadas a partir de la base de datos.

El siguiente es el código para la típica tabla comuna en nuestro país (Chile):
package Comuna;

#
# Hay un módulo que importa un objeto database handler para acceder a la base de datos
#
use General qw/$dbh/;

use strict;

#
# Este hash contiene las instancias de objetos comunas, será indexado por el id de comuna para obtener una instancia.
# se va cargando a medida que se producen materializaciones producto de las instanciaciones.
#
our %CACHE;

#
# Nuestro rutina de instanciación (método estático de clase), recibe un id de comuna y retorna la instancia correspondiente a ese id.
# En cualquier caso de no poder materializar retorna undef (falso).
#
sub new {

my ($class, $id_comuna) = @_;

return undef unless $id_comuna =~ /^\d+$/;

#
# Si la comuna entregada no ha sido materializada hay que materializar a partir de la base de datos e incorporar al cache
#
unless ($CACHE{$id_comuna}) {
my $sth = $dbh->prepare("select * from comuna where id_comuna = $id_comuna");
$sth->execute;
my $self = $sth->fetchrow_hashref;
return undef unless $self;

bless $self, $class;
#
# Incorporamos el objeto comuna recién instanciado al cache
#
$CACHE{$id_comuna} = $self;
}

#
# Siempre retornamos del cache
#
return $CACHE{$id_comuna};

}

Hay que considerar que para un número muy elevado de instancias puede llegar a usarse gran cantidad de memoria, en algunos casos debe ser balanceado con MaxRequestsPerChild, debido a que eventualmente todos los objetos terminarán en el Cache.

Optimizaciones

Inicializar todo el cache al cargar el Módulo

En mod_perl, podemos materializar todo el cache completo al cargar el módulo, usando PerlModule, de ese modo las semánticas de copy on write del sistema operativo permitiran compartir la memoria con los procesos Apache hijos. Este es el ejemplo:

our %CACHE;

my $sth = $dbh->prepare("select * from comuna");
$sth->execute;
while (my $row = $sth->fetchrow_hashref) {
$CACHE{$row->{id_comuna}} = bless $row, "Comuna";
}

Luego de la definición de la variable %CACHE, leemos todos los registros de la base de datos e inicializamos todo el cache. Esto se ejecutará al cargar por primera vez el módulo.

Esta prematerialización es conveniente cuando sabemos que el número de instancias es finitas y tenemos la memoria suficiente para almecenarlas.

Usar Shared Memory

Se puede usar alguno de los módulos de shared memory de perl y colocar ahí el hash que guarda el cache; eventualmente hacer esto usando PerlModule como en el caso anterior, permite asegurar la materialización pensando que copy on write puede eventualmente efectuar copias en algún momento.

Conclusión

Cuando este tipo de solución es usada en mod_perl logramos que cada objeto se instancie todas las veces que quiera, pero que sólo se materialize una vez  en la vida de cada proceso apache. Considerando que cada proceso apache atiende miles de requerimientos son exactamente  miles los accesos a la base de datos que se economizan.

Logramos que cada objeto se instancie todas las veces que quiera, pero que sólo se materialize una vez  en la vida de cada proceso apache.



Página construida con nvu, coloreo de sintaxis de código perl con Syntax::Highlight::Perl

Hans Poo, Consultor Linux, Desarrollo Web OpenSource
http://hans.opensource.cl, hans@opensource.cl, F: 672-93-18, 09-319.93.05
Santiago, Chile