Usare Memcached in Laravel

Memcached è un sistema di caching in memoria distribuito, ad alte prestazioni, che permette di memorizzare coppie chiave-valore direttamente nella RAM. Quando lo si integra in un'applicazione Laravel, si ottiene un notevole incremento delle performance perché le query più costose, i risultati di chiamate API esterne o le viste renderizzate possono essere serviti senza ripetere ogni volta le operazioni sottostanti. In questo articolo vedremo come configurare Memcached in Laravel, come utilizzarlo tramite la facade Cache e come applicarlo a casi d'uso reali come il caching di query Eloquent, il caching di sessioni e l'invalidazione mirata.

Installazione di Memcached sul sistema

Prima di poter utilizzare Memcached in Laravel, il servizio deve essere installato e in esecuzione sul server. Su una distribuzione Debian o Ubuntu si procede come segue:

# Aggiornamento dei pacchetti
sudo apt update

# Installazione del server Memcached
sudo apt install memcached

# Installazione dell'estensione PHP per Memcached
sudo apt install php-memcached

# Avvio e abilitazione del servizio
sudo systemctl start memcached
sudo systemctl enable memcached

Su macOS, tramite Homebrew, il procedimento è ancora più rapido:

# Installazione del server Memcached
brew install memcached

# Installazione dell'estensione PHP (verificare la versione di PHP installata)
pecl install memcached

# Avvio del servizio in background
brew services start memcached

Per verificare che Memcached sia effettivamente in ascolto sulla porta predefinita (11211), si può usare il seguente comando:

echo "stats" | nc localhost 11211

L'output mostrerà le statistiche del server, comprese le connessioni attive, gli hit, i miss e l'utilizzo della memoria. Se invece si riceve un errore di connessione rifiutata, è necessario verificare lo stato del servizio con systemctl status memcached.

Verifica dell'estensione PHP

Affinché Laravel possa comunicare con Memcached, l'estensione PHP memcached (con la d finale, non confondere con la vecchia estensione memcache) deve essere caricata. Si può verificare la presenza dell'estensione con:

php -m | grep memcached

Se l'estensione è correttamente installata, l'output mostrerà la stringa memcached. In caso contrario, è necessario installarla e riavviare PHP-FPM o il web server.

Configurazione di Laravel

Laravel supporta Memcached come driver di cache nativo. La configurazione si trova nel file config/cache.php, dove sono già presenti i parametri di default per il driver:

<?php

return [
    'default' => env('CACHE_STORE', 'memcached'),

    'stores' => [
        'memcached' => [
            'driver' => 'memcached',
            'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
            'sasl' => [
                env('MEMCACHED_USERNAME'),
                env('MEMCACHED_PASSWORD'),
            ],
            'options' => [
                // Esempio: timeout personalizzato in millisecondi
                // Memcached::OPT_CONNECT_TIMEOUT => 2000,
            ],
            'servers' => [
                [
                    'host' => env('MEMCACHED_HOST', '127.0.0.1'),
                    'port' => env('MEMCACHED_PORT', 11211),
                    'weight' => 100,
                ],
            ],
        ],
    ],
];

Nel file .env dell'applicazione si imposta il driver di cache come Memcached e si specificano host e porta:

CACHE_STORE=memcached
MEMCACHED_HOST=127.0.0.1
MEMCACHED_PORT=11211

Se si utilizza un servizio gestito che richiede autenticazione SASL (come AWS ElastiCache con SASL abilitato), si dovranno fornire anche MEMCACHED_USERNAME e MEMCACHED_PASSWORD. Per le installazioni locali, queste variabili possono essere lasciate vuote.

Operazioni di base con la facade Cache

Una volta configurato il driver, l'interazione con Memcached avviene esclusivamente tramite la facade Cache, che fornisce un'API uniforme indipendente dal backend sottostante. Vediamo le operazioni fondamentali.

Scrittura di un valore

<?php

use Illuminate\Support\Facades\Cache;

// Memorizza un valore per 600 secondi (10 minuti)
Cache::put('user_count', 1500, 600);

// Memorizza un valore senza scadenza (forever)
Cache::forever('app_version', '1.2.0');

Lettura di un valore

<?php

use Illuminate\Support\Facades\Cache;

// Lettura semplice (restituisce null se la chiave non esiste)
$userCount = Cache::get('user_count');

// Lettura con valore di default
$theme = Cache::get('preferred_theme', 'light');

// Verifica dell'esistenza della chiave
if (Cache::has('user_count')) {
    // La chiave esiste e non è scaduta
}

Pattern remember

Il metodo remember è il pattern più idiomatico in Laravel: tenta di leggere il valore dalla cache e, se non lo trova, esegue la closure passata, ne memorizza il risultato e lo restituisce. È ideale per il caching di query costose.

<?php

use Illuminate\Support\Facades\Cache;
use App\Models\Post;

$popularPosts = Cache::remember('popular_posts', 3600, function () {
    return Post::where('views', '>', 1000)
        ->orderByDesc('views')
        ->limit(10)
        ->get();
});

In questo esempio, la query Eloquent viene eseguita una sola volta e il risultato resta in cache per un'ora. Tutte le richieste successive serviranno il risultato direttamente da Memcached, riducendo drasticamente il carico sul database.

Cancellazione di una chiave

<?php

use Illuminate\Support\Facades\Cache;

// Rimuove una singola chiave
Cache::forget('popular_posts');

// Svuota completamente la cache (usare con cautela in produzione)
Cache::flush();

Caching di query Eloquent

Un caso d'uso tipico è il caching dei risultati di query frequentemente eseguite. Supponiamo di avere un modello Product e di voler memorizzare in cache la lista dei prodotti in evidenza, evitando di interrogare il database ad ogni richiesta:

<?php

namespace App\Http\Controllers;

use App\Models\Product;
use Illuminate\Support\Facades\Cache;

class ProductController extends Controller
{
    public function featured()
    {
        // Cache per 30 minuti
        $featuredProducts = Cache::remember('products.featured', 1800, function () {
            return Product::where('is_featured', true)
                ->with('category')
                ->orderBy('name')
                ->get();
        });

        return view('products.featured', [
            'products' => $featuredProducts,
        ]);
    }
}

Quando un prodotto viene aggiornato o eliminato, è importante invalidare la cache per evitare di servire dati obsoleti. Il modo più pulito per farlo è tramite gli eventi del modello, sfruttando un Observer:

php artisan make:observer ProductObserver --model=Product
<?php

namespace App\Observers;

use App\Models\Product;
use Illuminate\Support\Facades\Cache;

class ProductObserver
{
    public function saved(Product $product): void
    {
        // Invalidazione della cache quando un prodotto viene creato o aggiornato
        Cache::forget('products.featured');
    }

    public function deleted(Product $product): void
    {
        // Invalidazione anche in caso di cancellazione
        Cache::forget('products.featured');
    }
}

L'Observer viene registrato nel AppServiceProvider oppure direttamente sulla classe del modello tramite l'attributo #[ObservedBy]:

<?php

namespace App\Models;

use App\Observers\ProductObserver;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Model;

#[ObservedBy([ProductObserver::class])]
class Product extends Model
{
    // ...
}

Cache tag e limiti di Memcached

Laravel supporta i cache tag, che permettono di raggruppare logicamente più chiavi e di invalidarle in blocco. Memcached è uno dei driver che supporta questa funzionalità (a differenza, ad esempio, del driver file o database):

<?php

use Illuminate\Support\Facades\Cache;

// Scrittura con tag
Cache::tags(['products', 'featured'])->put('home_widget', $data, 3600);

// Lettura dei valori con tag
$widget = Cache::tags(['products', 'featured'])->get('home_widget');

// Invalidazione di tutte le chiavi associate al tag "products"
Cache::tags(['products'])->flush();

Va però sottolineato un limite intrinseco di Memcached: a differenza di Redis, Memcached non supporta nativamente operazioni atomiche su più chiavi o pattern matching sulle chiavi. Inoltre, Memcached non garantisce la persistenza dei dati: in caso di riavvio del servizio, tutta la cache viene persa. È quindi importante non utilizzare mai Memcached come unica fonte di verità per dati critici.

Utilizzo di Memcached per le sessioni

Memcached può essere utilizzato anche come driver per le sessioni dell'applicazione, il che è particolarmente utile in scenari multi-server dove le sessioni devono essere condivise tra più istanze. La configurazione si fa in config/session.php e nel file .env:

SESSION_DRIVER=memcached
SESSION_LIFETIME=120
SESSION_CONNECTION=memcached

Con questa configurazione, tutte le sessioni utente saranno memorizzate in Memcached invece che su file o database. Si ricordi tuttavia che, data la natura non persistente di Memcached, un riavvio del servizio comporterà il logout di tutti gli utenti.

Operazioni atomiche: increment e decrement

Memcached supporta operazioni atomiche di incremento e decremento di valori numerici, esposte da Laravel tramite i metodi increment e decrement. Sono particolarmente utili per implementare contatori thread-safe, come ad esempio un contatore di visualizzazioni:

<?php

namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Support\Facades\Cache;

class PostController extends Controller
{
    public function show(Post $post)
    {
        // Incremento atomico del contatore di visualizzazioni
        $views = Cache::increment("post.{$post->id}.views");

        return view('posts.show', [
            'post' => $post,
            'views' => $views,
        ]);
    }
}

Se la chiave non esiste, increment la inizializza a zero prima di incrementarla. È possibile passare un secondo argomento per specificare lo step di incremento (default: 1). Lo stesso meccanismo, in senso opposto, vale per decrement.

Configurazione di più server Memcached

In scenari ad alto traffico, è possibile distribuire la cache su più server Memcached. La libreria client implementa nativamente l'hashing consistente, distribuendo automaticamente le chiavi tra i nodi disponibili. La configurazione richiede semplicemente di aggiungere più entry nell'array servers:

<?php

'memcached' => [
    'driver' => 'memcached',
    'servers' => [
        [
            'host' => '10.0.0.1',
            'port' => 11211,
            'weight' => 100,
        ],
        [
            'host' => '10.0.0.2',
            'port' => 11211,
            'weight' => 100,
        ],
        [
            'host' => '10.0.0.3',
            'port' => 11211,
            'weight' => 100,
        ],
    ],
],

Il parametro weight permette di assegnare un peso relativo ai vari server, utile quando i nodi hanno capacità di memoria differenti. Un server con peso 200 riceverà circa il doppio delle chiavi rispetto a uno con peso 100.

Debugging e monitoraggio

Per analizzare l'efficacia della cache, è utile monitorare le statistiche di Memcached. Il comando stats via telnet (o netcat) fornisce informazioni dettagliate:

echo "stats" | nc localhost 11211

I valori più rilevanti sono get_hits (richieste servite dalla cache) e get_misses (richieste per chiavi non presenti). Un rapporto hit/miss elevato indica una cache ben dimensionata e con TTL appropriati. Se invece il rapporto miss è alto, conviene rivedere la strategia di caching: probabilmente le chiavi scadono troppo in fretta oppure non vengono riutilizzate.

Dal lato Laravel, durante lo sviluppo si può utilizzare Laravel Telescope, che traccia tutte le operazioni di cache (hit, miss, write, forget) e ne mostra timing e payload, facilitando il debugging.

Memcached o Redis?

Una domanda ricorrente è quando preferire Memcached a Redis. Memcached eccelle nel caso d'uso classico del caching puro: chiavi semplici, valori serializzati, TTL definiti. È più leggero in termini di memoria e ha un overhead minore. Redis, d'altro canto, offre persistenza, strutture dati avanzate (liste, set, hash, stream), supporto a pub/sub e operazioni più sofisticate sulle chiavi.

Se l'unico obiettivo è il caching di query e oggetti serializzati, Memcached è perfettamente adeguato e spesso più performante in termini di throughput puro. Se invece servono funzionalità come code di lavoro, locking distribuito, leaderboard o pub/sub, Redis è la scelta naturale.

Conclusione

Integrare Memcached in Laravel è un'operazione semplice e dai benefici immediati. La facade Cache astrae completamente il driver sottostante, permettendo di adottare Memcached per il caching di query, sessioni, contatori atomici e dati condivisi tra istanze, con un'API consistente e ben documentata. Una volta interiorizzati pattern come remember e l'invalidazione tramite Observer, è possibile ottenere un significativo miglioramento delle performance senza alterare la logica di business. L'importante è ricordare i limiti del backend — assenza di persistenza, niente pattern matching sulle chiavi, niente strutture dati complesse — e progettare la strategia di caching tenendone conto.