APCu en PHP: caché en memoria de usuario con apc_store, apc_fetch y apc_delete

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=1 en php.ini.
  • Tamaño de la caché: si se llena, APCu expulsa entradas antiguas. Monitoriza apcu_cache_info()['expunges']; si sube, aumenta apc.shm_size.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP