APCu (APC User Cache) es una caché en memoria que almacena datos en el proceso PHP del servidor web. A diferencia de Redis o Memcached, no requiere instalar ningún servicio externo: los datos viven en la memoria compartida del proceso PHP y cualquier petición posterior puede acceder a ellos sin tocar la base de datos ni el disco.
¿Cuándo usar APCu y cuándo Redis?
APCu es ideal cuando:
- Solo tienes un servidor web (APCu no comparte datos entre servidores).
- Quieres cachear datos de configuración, resultados de consultas frecuentes o cálculos costosos.
- No puedes instalar servicios adicionales (hosting compartido, restricciones de red).
Redis es mejor cuando tienes varios servidores, necesitas estructuras de datos más complejas (listas, sets ordenados) o quieres persistencia.
Instalar APCu
# Ubuntu/Debian
sudo apt install php-apcu
# Con PECL
pecl install apcu
# Añadir en php.ini
extension=apcu.so
apc.enabled=1
apc.shm_size=64M ; memoria compartida
apc.ttl=3600 ; TTL por defecto en segundos
<?php
// Verificar que está disponible
if (!extension_loaded('apcu') || !apcu_enabled()) {
throw new RuntimeException('APCu no está disponible');
}
apcu_store y apcu_fetch
<?php
// Guardar con TTL de 300 segundos
apcu_store('clave', 'valor', 300);
// Guardar un array
apcu_store('config_app', [
'nombre' => 'Mi Aplicación',
'version' => '2.1.0',
'debug' => false,
], 3600);
// Recuperar
$valor = apcu_fetch('clave');
if ($valor === false) {
echo 'No existe en caché o expiró';
}
// Verificar existencia sin recuperar el valor
if (apcu_exists('clave')) {
echo 'Existe en caché';
}
// Recuperar con indicador de éxito
$exito = false;
$datos = apcu_fetch('config_app', $exito);
if (!$exito) {
// Regenerar datos
$datos = cargarConfigDesdeDB();
apcu_store('config_app', $datos, 3600);
}
Patrón cache-aside con APCu
El patrón más habitual: primero busca en caché, si no está lo obtiene de la fuente original y lo guarda:
<?php
function obtenerCategorias(PDO $pdo): array {
$clave = 'categorias_menu';
$hit = false;
$datos = apcu_fetch($clave, $hit);
if ($hit) {
return $datos;
}
// Caché miss: consultar la BD
$datos = $pdo->query('SELECT id, nombre, slug FROM categorias WHERE activa = 1 ORDER BY orden')
->fetchAll(PDO::FETCH_ASSOC);
apcu_store($clave, $datos, 600); // 10 minutos
return $datos;
}
// Invalidar la caché cuando cambian los datos
function guardarCategoria(PDO $pdo, array $datos): void {
// ... INSERT/UPDATE en BD ...
apcu_delete('categorias_menu'); // forzar recarga en la próxima petición
}
apcu_add: guardar solo si no existe
La diferencia con apcu_store: apcu_add falla si la clave ya existe. Útil para implementar locks simples:
<?php
// Lock simple para evitar procesos paralelos
function procesarConLock(string $tarea, callable $fn): mixed {
$clave_lock = 'lock_' . $tarea;
// Si la clave ya existe, otro proceso está trabajando
if (!apcu_add($clave_lock, 1, 30)) { // TTL 30 segundos como seguridad
throw new RuntimeException("La tarea '$tarea' ya está en curso");
}
try {
return $fn();
} finally {
apcu_delete($clave_lock); // liberar el lock siempre, incluso si hay excepción
}
}
// Uso
procesarConLock('enviar_informe', function() {
// Solo se ejecuta si no hay otro proceso activo
generarYEnviarInforme();
});
apcu_inc y apcu_dec: contadores atómicos
Las operaciones de incremento y decremento en APCu son atómicas, lo que las hace seguras en entornos concurrentes:
<?php
// Contador de visitas de una página
$visitas = apcu_inc('visitas_articulo_42', 1, $exito);
if (!$exito) {
// La clave no existía, inicializamos
apcu_store('visitas_articulo_42', 1, 86400); // 24 horas
$visitas = 1;
}
echo "Este artículo tiene $visitas visitas hoy";
// Rate limiting básico (máx 100 peticiones por IP por minuto)
function checkRateLimit(string $ip): bool {
$clave = 'rate_' . md5($ip) . '_' . date('YmdHi'); // clave por minuto
$total = apcu_inc($clave, 1, $exito);
if (!$exito) {
apcu_store($clave, 1, 60);
return true;
}
return $total <= 100;
}
if (!checkRateLimit($_SERVER['REMOTE_ADDR'])) {
http_response_code(429);
exit('Too Many Requests');
}
// Decremento (no baja de 0)
apcu_dec('stock_producto_5', 1);
apcu_fetch múltiple
<?php
// Obtener varias claves de una vez
$claves = ['config_db', 'config_mail', 'config_app'];
$resultados = apcu_fetch($claves, $exito);
// $resultados es un array asociativo con las claves encontradas
// $exito es true si todas se encontraron
$no_encontradas = array_diff($claves, array_keys($resultados));
// Cargar las que faltan de la fuente original
apcu_store múltiple y apcu_delete
<?php
// Guardar varias claves de una vez
$datos = [
'clave1' => 'valor1',
'clave2' => 'valor2',
'clave3' => [1, 2, 3],
];
$fallidas = apcu_store($datos, null, 300);
// Devuelve array de claves que fallaron al guardarse
// Eliminar
apcu_delete('clave1');
// Eliminar varias
apcu_delete(new APCuIterator('/^config_/')); // elimina todas las que empiezan por "config_"
// Limpiar toda la caché de usuario
apcu_clear_cache();
Inspeccionar la caché
<?php
// Estadísticas generales
$info = apcu_cache_info();
echo "Entradas en caché: " . $info['num_entries'];
echo "Memoria usada: " . round($info['mem_size'] / 1024 / 1024, 2) . " MB";
echo "Hits: " . $info['num_hits'];
echo "Misses: " . $info['num_misses'];
// Iterar sobre las entradas
$iter = new APCuIterator('/^visitas_/', APC_ITER_ALL);
foreach ($iter as $clave => $entrada) {
echo "$clave: {$entrada['value']} (TTL: {$entrada['ttl']})n";
}
Consideraciones importantes
- No persiste entre reinicios: al reiniciar PHP-FPM o Apache se pierde toda la caché. Diseña tu código para que funcione correctamente con caché fría.
- No comparte datos entre servidores: en un cluster con balanceo de carga, cada servidor tiene su propio APCu. Usa Redis/Memcached en ese caso.
- CLI vs web: el APCu de la CLI y el del servidor web son instancias separadas. Para que funcione en CLI:
apc.enable_cli=1en php.ini. - Tamaño de la caché: si se llena, APCu expulsa entradas antiguas. Monitoriza
apcu_cache_info()['expunges']; si sube, aumentaapc.shm_size.
