PHP: Cache en tablas MySQL
Introducción
Es bastante frecuente que nuestras aplicaciones PHP extraen datos de nuestra base para generar listas como Menues o Selects, extraer datos de un webservice u otros, los que en general se mantienen constantes. Cada vez que se carga dicha página, dicho bloque de datos debe ser generado, y finalmente impactar en el tiempo de respuesta del usuario o los recursos utilizados en el servidor.
La opción que se plantea aquí es construir dicho bloque una sola vez y dejarlo disponible por un tiempo en que confiamos en que el dato no cambia o no nos impacte si la información cambia en ese lapso, por ejemplo:
- Listado de comunas: Frecuentemente al solicitar alguna dirección, generamos un <select> con las comunas para la región, el que podemos dejar generado por 24 horas
- Información del dia: Tener un cuadro con datos como el santoral del dá, valores de UF, Dolar. Tenemos confianza que dicha información se puede mantener constante hasta el final del día
- Datos del turno: Presentar por ejemplo datos del supervisor de turno, que sabemos podemos mantener por 5 minutos
- Últimos Tweets: Podemos cargar la lista de los últimos tweets y conservarlos por 1 minuto
Aunque existen producctos específicos para cache de éste tipo, como Memcached o APC nativo de PHP, en ocaciones no se puede incluir el software en el servidor, o se prefiere no tener dependencias adicionales.
Descripción de la propuesta
La proposición consiste en disponer una tabla donde almacenaremos los datos generados basados en un nombre y una referencia de fecha que nos indique hasta cuando es válido dicho dato de cache:
DROP TABLE IF EXISTS `mycache`;
CREATE TABLE `mycache` (
`name` varchar(50) NOT NULL,
`data` varchar(8000) NOT NULL,
`updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`validto` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
PRIMARY KEY (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
Los fragmentos de código a generar, se invocan mediante funciones en PHP que se encarguen
de buscar el dato en el cache y se es fresco (validto > now()
) se usa sin mayor procesamiento.
En caso contrario, nos encargamos de generar los tags html, lo almacenamos en el cache y retornamos el
texto generado.
Mejor con un ejemplo
Para efectos de un ejemplo, consideraremos la generación de 2 combo <select> de 30 elementos, los que se extraen de la tabla combotbl con la siguiente estructura:
DROP TABLE IF EXISTS `combotbl`;
CREATE TABLE `combotbl` (
`ID` int(11) NOT NULL AUTO_INCREMENT,
`tipo` int(11) NOT NULL,
`value` varchar(60) NOT NULL,
`description` varchar(120) NOT NULL,
PRIMARY KEY (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
Los valores de cada combo se relacionan por el campo tipo, donde se determina el valor y descripción, así, cada combo se forma mediante la función creacombo:
<html>
<head>
<title>
Prueba de Cache base de datos
</title>
</head>
<body>
<h1>
Ejemplo de cache en base de datos
</h1>
<form name="test">
<?php
$db = mysqli_connect('localhost','test','password','test');
if (mysqli_connect_errno()) {
echo "Failed to connect to MySQL: " . mysqli_connect_error();
}
$tini = microtime(TRUE);
$c1 = creacombo($db,"combo1", 1) ;
$c2 = creacombo($db,"combo2", 2) ;
$tiempo = (microtime(TRUE) - $tini) * 1000;
echo "<div>Combo1: " . $c1 . "</div>\n" ;
echo "<div>Combo2: " . $c2 . "</div>\n" ;
echo "<br /><br />" ;
echo "<div>Tiempo: $tiempo miliegundos" ;
function creacombo($db,$name,$tipo) {
$sql = "Select * from combotbl where tipo=$tipo order by value" ;
$result = mysqli_query($db,$sql);
$combo = "<select id=\"$name\">\n";
while($row = mysqli_fetch_array($result)) {
$combo .= '<option value="'. $row['value'] . '">' .
$row['description'] . '</option>' . "\n";
}
$combo .= "</select>\n" ;
return $combo;
}
?>
</form>
</body>
</html>
Al realizar las pruebas, el tiempo de utilizado para generar ambos combos es del órden de 1.5 milisegundos
Ahora es donde entra a operar el cache, agregamos las funciones cache_get y cache_set las que se encargarán de recuperar y guardar los datos en el cache. Ahora, para obtener los datos seguimos los siguientes pasos:
- Verificamos si el dato existe en el cache y se encuentra fresco, de ser así, se retorna el valor almacenado sin mayor procesamiento.
- Dado que el dato no existe o no está actualizado, se debe generar a partir de los datos en la base (o WebService externo)
- Almacenamos el valor generado en la base de cache, asignando el tiempo de duración
- Retornamos el valor generado
Revicemos entonces como queda la programación de la página utilizando el cache:
<html>
<head>
<title>
Prueba de Cache base de datos
</title>
</head>
<body>
<h1>
Ejemplo de cache en base de datos
</h1>
<form name="test">
<?php
$db = mysqli_connect('localhost','test','password','test');
if (mysqli_connect_errno()) {
echo "Failed to connect to MySQL: " . mysqli_connect_error();
}
$tini = microtime(TRUE);
$c1 = creacombo($db,"combo1", 1) ;
$c2 = creacombo($db,"combo2", 2) ;
$tiempo = (microtime(TRUE) - $tini) * 1000;
echo "<div>Combo1: " . $c1 . "</div>\n" ;
echo "<div>Combo2: " . $c2 . "</div>\n" ;
echo "<br /><br />" ;
echo "<div>Tiempo: $tiempo miliegundos" ;
function creacombo($db,$name,$tipo) {
$combo = cache_get($db, $name);
if ( empty($combo) ) {
$sql = "Select * from combotbl where tipo=$tipo order by value" ;
$result = mysqli_query($db,$sql);
$combo = "<select id=\"$name\">\n";
while($row = mysqli_fetch_array($result)) {
$combo .= '<option value="'. $row['value'] . '">' .
$row['description'] . '</option>' . "\n";
}
$combo .= "</select>\n" ;
cache_set($db, $name, $combo, 3600) ;
}
return $combo;
}
function cache_get($db,$name) {
$sql = "select * from mycache where name = '$name' and validto > now()";
$result = mysqli_query($db,$sql);
if ( empty($result) ) {
return "" ;
} else {
$row = mysqli_fetch_array($result) ;
return $row['data'] ;
}
}
function cache_set($db,$name,$value,$tiempo) {
$sql = "insert into mycache set name = '$name',
validto = now()+ INTERVAL $tiempo SECOND,
data = '$value'
on duplicate key update
validto = values(validto),
data = values(data)";
$result = mysqli_query($db,$sql);
}
?>
</form>
</body>
</html>
Al cargar la URL concache.php, obtenemos en la primera ejecución tiempos del órden de 3 milisegundos, pero en ejecuciones posteriores, nuestro tiempo baja a los 0.9 milisegundos, lo que significa un 66% más rápido.
Si necesitamos limpiar el cache, basta con eliminar el registro en la tabla o simplemente hacemos
un TRUNCATE
a la tabla mycache
para eliminar todo el cache.
Conclusión
Podemos ver que éste mecanismo nos trae beneficios en los tiempos de respuesta para contenidos repetitivos. Mucho dependerá de los tiempos asignados para el cache y del tipo de datos almacenados, teniendo mayores beneficios de aquellos que son más costosos de obtener, como consultas externas a WebServices, o estadísticas.
Se debe tener consideración al momento de determinar el tiempo de permanencia en cache en los siguientes aspectos:
- Tiempos Bajos: Los datos se encuentran actualizados, pero al vencer el cache demorará más tiempo en generar la información.
- Tiempos Altos: La generación en general será rápida, pero podría quedar desactualizada.
Mejor con APC, cuando sea posible
PHP incluye un módulo nativo para Cache, que maneja los datos en memoria compartida y archivos, pero requiere ser instalado. En CentOS es bastante simple:
yum install php-pecl-apc
podemos encontrar su documentación en http://cl1.php.net/manual/es/book.apc.php. Las funciones más importantes a utilizar son:
- apc_fetch($key): Recupera el valor de la variable
$key
del cache, retorna el valor almacenado o FALSE si no existe - apc_store($key,$val,$t): Almacena la variable
$key
con valor$val
por$t
segundos
Utilizando el mismo ejemplo anterior, nuestro código PHP queda como:
<html>
<head>
<title>
Prueba de Cache "APC"
</title>
</head>
<body>
<h1>
Ejemplo de cache APC
</h1>
<form name="test">
<?php
$db = mysqli_connect('localhost','test','password','test');
if (mysqli_connect_errno()) {
echo "Failed to connect to MySQL: " . mysqli_connect_error();
}
$tini = microtime(TRUE);
$c1 = creacombo($db,"combo1", 1) ;
$c2 = creacombo($db,"combo2", 2) ;
$tiempo = (microtime(TRUE) - $tini) * 1000;
echo "<div>Combo1: " . $c1 . "</div>\n" ;
echo "<div>Combo2: " . $c2 . "</div>\n" ;
echo "<br /><br />" ;
echo "<div>Tiempo: $tiempo miliegundos" ;
function creacombo($db,$name,$tipo) {
$combo = apc_fetch($name);
if ( !($combo) ) {
$sql = "Select * from combotbl where tipo=$tipo order by value" ;
$result = mysqli_query($db,$sql);
$combo = "<select id=\"$name\">\n";
while($row = mysqli_fetch_array($result)) {
$combo .= '<option value="'. $row['value'] . '">' .
$row['description'] . '</option>' . "\n";
}
$combo .= "</select>\n" ;
apc_store($name, $combo, 3600) ;
}
return $combo;
}
?>
</form>
</body>
</html>
Con lo que el tiempo de carga de los combo, pasa a ¡ 0.09 milisegundos !!!, 16 veces más rápido.
Si tenemos administración de nuestro servicio PHP, lo mejor será utilizar APC, teniendo en consideración que los datos almacenados en el APC son accesibles desde cualquier script PHP dentro del servidor, incluso de otros dominios, por lo que es un riesgo de seguridad si el servidor es compartido.
Generado por Sistema y almacenado en cache
Wyzer Luis Hernán de la Barra |
|
Teléfono: | +56995451689 |
WhatsApp: | +56995451689 |
E-Mail: | info@wyzer.cl |
Web: | www.wyzer.cl |