Redis: Benchmark, Tuning e Configurazione su Ubuntu

Redis

Redis è un sistema di gestione di dati in-memory open source, utilizzato come database, cache e message broker. Sviluppato da Salvatore Sanfilippo e oggi mantenuto da Redis Ltd., si basa su una struttura dati chiave-valore con supporto a stringhe, hash, liste, set e molto altro. È noto per la sua velocità, affidabilità e versatilità, ed è ampiamente adottato in ambienti web, spesso come cache per database o per gestire sessioni e code.

MySQL e PostgreSQL sono database relazionali (RDBMS) che memorizzano i dati su disco. Redis, al contrario, lavora interamente in RAM, il che lo rende molto più veloce ma anche meno adatto per dati che devono persistere per decenni.

In questa guida useremo come server di test il YouYeeToo K1, precedentemente racensito ed andremo a trattare:

  1. Pro e contro dell’uso di Redis come cache
  2. Installazione di Redis e MySQL con Docker Compose
  3. Creazione dei Dati Mockup per i Test
  4. Performance Senza Redis
  5. Performance Con Redis
  6. Analisi dei Risultati: Redis vs MySQL
  7. Tuning di Redis per le Massime Performance
  8. Conclusioni

Pro e contro dell’uso di Redis come cache

L’adozione di Redis come cache, come ogni scelta tecnica, presenta vantaggi e svantaggi da valutare con attenzione.

Vantaggi principali:

  • Velocità: Redis è in-memory, quindi i tempi di risposta sono sub-millisecondi. Un database su disco è nell’ordine dei millisecondi.
  • Riduzione del carico sul database: Spostando le query più pesanti su Redis, il database primario respira.
  • Semplicità: Redis è facile da installare e configurare. La curva di apprendimento è bassa.

Svantaggi e criticità:

  • Memoria limitata: Redis è vincolato dalla RAM disponibile. Non puoi cacheare tutto.
  • Persistenza debole: Anche se supporta RDB e AOF, Redis non è un sostituto di un database relazionale per dati critici.
  • Complessità aggiuntiva: Aggiungere Redis significa introdurre un nuovo componente nel tuo stack, con la sua gestione e il suo monitoring.

Installazione di Redis e MySQL con Docker Compose

Invece di installare Redis e MySQL separatamente sul sistema, useremo Docker Compose per un fatto di comodità. Questo ci permette di:

  • Avere un ambiente isolato e riproducibile
  • Distruggere e ricreare tutto con un comando
  • Eseguire i test su qualsiasi macchina

Crea una cartella per il progetto e all’interno un file docker-compose.yml:

version: '3.8'

services:
  mysql:
    image: mysql:8.0
    container_name: redis-benchmark-mysql
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: benchmark
      MYSQL_USER: benchmark
      MYSQL_PASSWORD: benchmark
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql
    networks:
      - benchmark-net
    restart: unless-stopped

  redis:
    image: redis:7.2-alpine
    container_name: redis-benchmark-redis
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    networks:
      - benchmark-net
    restart: unless-stopped
    command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru

  phpmyadmin:
    image: phpmyadmin/phpmyadmin
    container_name: redis-benchmark-phpmyadmin
    environment:
      PMA_HOST: mysql
      PMA_PORT: 3306
      UPLOAD_LIMIT: 100M
    ports:
      - "8080:80"
    networks:
      - benchmark-net
    depends_on:
      - mysql
    restart: unless-stopped

networks:
  benchmark-net:
    driver: bridge

volumes:
  mysql_data:
  redis_data:
Servizio Descrizione
MySQL Database relazionale per i test. Porta 3306 esposta.
Redis Cache in-memory con persistenza AOF attivata e limite di 256MB.
phpMyAdmin (Opzionale) Per visualizzare i dati del database. Porta 8080.

Parametri di Redis già ottimizzati:

  • --appendonly yes: persistenza attiva (AOF)
  • --maxmemory 256mb: limite di memoria per evitare crash
  • --maxmemory-policy allkeys-lru: elimina le chiavi meno usate quando la memoria è piena

Connessione ai servizi:

Servizio Host Porta Credenziali
MySQL localhost 3306 user: benchmark / pass: benchmark
Redis localhost 6379 nessuna password
phpMyAdmin localhost 8080 user: benchmark / pass: benchmark
# Avvia i container in background
docker-compose up -d

# Verifica che siano in esecuzione
docker-compose ps

# Visualizza i log
docker-compose logs -f

Perchè questa scelta

  • Riproducibilità: chiunque può eseguire i test con un singolo comando
  • Pulizia: i dati sono in volumi Docker, facilmente eliminabili
  • Performance: Redis e MySQL in container hanno overhead minimo
  • Flessibilità: puoi cambiare versione di Redis/MySQL modificando l’immagine
Redis è pronto

Con Docker Compose hai Redis e MySQL pronti per i test. Nel prossimo capitolo creeremo un dataset mockup per iniziare i benchmark.

redis-docker-compose

Creazione dei Dati Mockup per i Test

Per fare un benchmark serio, non possiamo testare con 10 righe. Dobbiamo lavorare con un dataset realistico. In questo capitolo creeremo una tabella con 100.000 record fittizi e scriveremo uno script per eseguire le query di test ( rigorosamente in PHP ). La struttura della tabella Creeremo una tabella orders (ordini) che simula i dati di un e-commerce. È un caso d’uso comune e rappresentativo.
CREATE TABLE orders (
    id INT AUTO_INCREMENT PRIMARY KEY,
    customer_name VARCHAR(100),
    customer_email VARCHAR(100),
    product_name VARCHAR(200),
    quantity INT,
    price DECIMAL(10, 2),
    order_date DATE,
    status VARCHAR(20),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Lo script per generare i dati

Ho scritto uno script PHP che genera 100.000 record fittizi, ma volendo si può usare qualsiasi linguaggio. È semplice, veloce e usa il generatore di nomi e parole.

<?php

// Generatore di dati mockup per benchmark Redis
// Esegui: php generate_mockup.php

$host = '127.0.0.1';
$user = 'benchmark';
$pass = 'benchmark';
$db   = 'benchmark';

$pdo = new PDO("mysql:host=$host;dbname=$db;charset=utf8", $user, $pass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

// Nomi e prodotti fittizi
$firstNames = ['Marco', 'Luigi', 'Giuseppe', 'Antonio', 'Francesco', 'Andrea', 'Elena', 'Sofia', 'Giulia', 'Alessia'];
$lastNames = ['Rossi', 'Bianchi', 'Verdi', 'Romani', 'Ferrari', 'Colombo', 'Rizzo', 'Lombardi', 'Mancini', 'Marino'];
$products = ['Prodotto Alpha', 'Prodotto Beta', 'Prodotto Gamma', 'Prodotto Delta', 'Prodotto Epsilon', 
             'Prodotto Zeta', 'Prodotto Eta', 'Prodotto Theta', 'Prodotto Iota', 'Prodotto Kappa'];
$statuses = ['pending', 'processing', 'shipped', 'delivered', 'cancelled'];

echo "Generazione dati mockup...\n";

$pdo->beginTransaction();

$stmt = $pdo->prepare("INSERT INTO orders (customer_name, customer_email, product_name, quantity, price, order_date, status) VALUES (?, ?, ?, ?, ?, ?, ?)");

for ($i = 1; $i <= 100000; $i++) {
    $firstName = $firstNames[array_rand($firstNames)];
    $lastName = $lastNames[array_rand($lastNames)];
    $name = $firstName . ' ' . $lastName;
    $email = strtolower($firstName . '.' . $lastName . '@example.com');
    $product = $products[array_rand($products)];
    $quantity = rand(1, 10);
    $price = rand(1000, 50000) / 100; // Prezzi da 10.00 a 500.00
    $date = date('Y-m-d', strtotime('-' . rand(0, 365) . ' days'));
    $status = $statuses[array_rand($statuses)];
    
    $stmt->execute([$name, $email, $product, $quantity, $price, $date, $status]);
    
    if ($i % 10000 == 0) {
        echo "Inseriti $i record...\n";
    }
}

$pdo->commit();

echo "Generazione completata: 100.000 record inseriti.\n";

Verifica dei dati e preparazione per i test di benchmark

Dopo l’inserimento, verifichiamo che tutto funzioni e prepariamo 5 query per i vari test di benchmark:

-- Conta i record
SELECT COUNT(*) FROM orders;
-- Dovrebbe restituire: 100000

-- Vedi un campione
SELECT * FROM orders LIMIT 10;

-- Verifica la distribuzione
SELECT status, COUNT(*) FROM orders GROUP BY status;

-- Conteggio per "stato"
SELECT status, COUNT(*) as total FROM orders GROUP BY status;

-- Totale vendite per mese
SELECT DATE_FORMAT(order_date, '%Y-%m') as month, SUM(price * quantity) as total FROM orders GROUP BY month ORDER BY month DESC;

-- TOP 10 clienti per spesa
SELECT customer_name, SUM(price * quantity) as total_spent FROM orders GROUP BY customer_name ORDER BY total_spent DESC LIMIT 10;

-- Ordini di cliente specifico ( da specificare dopo aver controllato il proprio dataset!! )
SELECT * FROM orders WHERE customer_email LIKE '%marco.rossi%';

-- Statistiche mensili
SELECT DATE_FORMAT(order_date, '%Y-%m') as month, 
       COUNT(*) as orders,
       SUM(price * quantity) as revenue,
       AVG(price * quantity) as avg_order_value
FROM orders 
GROUP BY month 
ORDER BY month DESC;

Performance senza Redis

Prima di attivare Redis, dobbiamo misurare le performance del database “nudo”. Questo ci darà un termine di paragone per valutare il miglioramento reale.

Utilizzeremo 5 query rappresentative di un’applicazione reale:

  • Conteggio per “stato”
  • Totale vendite per mese
  • TOP 10 clienti per spesa
  • Ordini di uncliente specifico
  • Statistiche mensili

Script di benchmark

Creiamo uno script PHP che esegue ogni query 100 volte e calcola il tempo medio ed eseguiamolo.

<?php
// benchmark_baseline.php
// Esegui: php benchmark_baseline.php

$host = '127.0.0.1';
$user = 'benchmark';
$pass = 'benchmark';
$db   = 'benchmark';

try {
    $pdo = new PDO("mysql:host=$host;dbname=$db;charset=utf8", $user, $pass);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
    die("Errore di connessione: " . $e->getMessage());
}

// Le 5 query da testare
$queries = [
    'Conteggio per stato' => "SELECT status, COUNT(*) as total FROM orders GROUP BY status",
    'Vendite per mese' => "SELECT DATE_FORMAT(order_date, '%Y-%m') as month, SUM(price * quantity) as total FROM orders GROUP BY month ORDER BY month DESC",
    'Top 10 clienti' => "SELECT customer_name, SUM(price * quantity) as total_spent FROM orders GROUP BY customer_name ORDER BY total_spent DESC LIMIT 10",
    'Ricerca per email' => "SELECT * FROM orders WHERE customer_email LIKE '%marco.rossi%'",
    'Statistiche mensili' => "SELECT DATE_FORMAT(order_date, '%Y-%m') as month, COUNT(*) as orders, SUM(price * quantity) as revenue, AVG(price * quantity) as avg_order_value FROM orders GROUP BY month ORDER BY month DESC"
];

$iterations = 100;
$results = [];

echo "Esecuzione benchmark baseline (senza Redis)...\n";
echo "Query eseguite $iterations volte ciascuna.\n\n";

foreach ($queries as $name => $sql) {
    $times = [];
    
    for ($i = 0; $i < $iterations; $i++) {
        $start = microtime(true);
        $pdo->query($sql)->fetchAll();
        $end = microtime(true);
        $times[] = ($end - $start) * 1000; // Converti in millisecondi
    }
    
    $avg = array_sum($times) / count($times);
    $min = min($times);
    $max = max($times);
    
    $results[$name] = [
        'avg' => $avg,
        'min' => $min,
        'max' => $max
    ];
    
    echo "🔹 $name\n";
    echo "   Media: " . number_format($avg, 2) . " ms\n";
    echo "   Min:   " . number_format($min, 2) . " ms\n";
    echo "   Max:   " . number_format($max, 2) . " ms\n\n";
}

// Salva i risultati per il confronto successivo
file_put_contents('baseline_results.json', json_encode($results, JSON_PRETTY_PRINT));
echo "✅ Risultati salvati in baseline_results.json\n";

Performance con Redis

Ora che abbiamo una baseline chiara, attiviamo Redis come cache e ripetiamo gli stessi test. Vedremo quanto migliorano i tempi di risposta.

Come funziona la cache con Redis

Lo schema è semplice:

  1. Prima di eseguire una query, controlliamo se il risultato è già in cache
  2. Se sì, lo restituiamo immediatamente (senza toccare il database)
  3. Se no, eseguiamo la query, salviamo il risultato in Redis e lo restituiamo
<?php
// benchmark_redis.php
// Esegui: php benchmark_redis.php

// Installa predis se non lo hai: composer require predis/predis
require 'vendor/autoload.php';

$host = '127.0.0.1';
$user = 'benchmark';
$pass = 'benchmark';
$db   = 'benchmark';

// Connessione al database
try {
    $pdo = new PDO("mysql:host=$host;dbname=$db;charset=utf8", $user, $pass);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
    die("Errore di connessione al database: " . $e->getMessage());
}

// Connessione a Redis
try {
    $redis = new Predis\Client([
        'scheme' => 'tcp',
        'host'   => '127.0.0.1',
        'port'   => 6379,
    ]);
    $redis->ping(); // Verifica che Redis risponda
    echo "✅ Connessione a Redis stabilita.\n\n";
} catch (Exception $e) {
    die("Errore di connessione a Redis: " . $e->getMessage());
}

// Le stesse 5 query di prima
$queries = [
    'Conteggio per stato' => "SELECT status, COUNT(*) as total FROM orders GROUP BY status",
    'Vendite per mese' => "SELECT DATE_FORMAT(order_date, '%Y-%m') as month, SUM(price * quantity) as total FROM orders GROUP BY month ORDER BY month DESC",
    'Top 10 clienti' => "SELECT customer_name, SUM(price * quantity) as total_spent FROM orders GROUP BY customer_name ORDER BY total_spent DESC LIMIT 10",
    'Ricerca per email' => "SELECT * FROM orders WHERE customer_email LIKE '%marco.rossi%'",
    'Statistiche mensili' => "SELECT DATE_FORMAT(order_date, '%Y-%m') as month, COUNT(*) as orders, SUM(price * quantity) as revenue, AVG(price * quantity) as avg_order_value FROM orders GROUP BY month ORDER BY month DESC"
];

// Funzione che esegue la query con cache
function executeWithCache($pdo, $redis, $sql, $cacheKey) {
    // Prova a leggere dalla cache
    $cached = $redis->get($cacheKey);
    
    if ($cached !== null) {
        // Cache hit: restituisci i dati dalla cache
        return [
            'cached' => true,
            'data' => json_decode($cached, true)
        ];
    }
    
    // Cache miss: esegui la query sul database
    $start = microtime(true);
    $result = $pdo->query($sql)->fetchAll();
    $queryTime = (microtime(true) - $start) * 1000;
    
    // Salva in Redis con scadenza di 60 secondi
    $redis->setex($cacheKey, 60, json_encode($result));
    
    return [
        'cached' => false,
        'data' => $result,
        'query_time' => $queryTime
    ];
}

$iterations = 100;
$results = [];

echo "Esecuzione benchmark CON Redis (caching)...\n";
echo "Query eseguite $iterations volte ciascuna.\n";
echo "(Prima esecuzione: cache miss, successive: cache hit)\n\n";

foreach ($queries as $name => $sql) {
    $times = [];
    $cacheKey = 'benchmark_' . md5($sql);
    
    // Svuota la cache prima del test
    $redis->del($cacheKey);
    
    for ($i = 0; $i < $iterations; $i++) {
        $start = microtime(true);
        $result = executeWithCache($pdo, $redis, $sql, $cacheKey);
        $end = microtime(true);
        $times[] = ($end - $start) * 1000;
    }
    
    $avg = array_sum($times) / count($times);
    $min = min($times);
    $max = max($times);
    
    // Calcola separatamente la prima esecuzione (cache miss)
    $first = $times[0];
    
    $results[$name] = [
        'avg' => $avg,
        'min' => $min,
        'max' => $max,
        'first' => $first
    ];
    
    echo "🔹 $name\n";
    echo "   Prima esecuzione (cache miss): " . number_format($first, 2) . " ms\n";
    echo "   Media (con cache): " . number_format($avg, 2) . " ms\n";
    echo "   Min:   " . number_format($min, 2) . " ms\n";
    echo "   Max:   " . number_format($max, 2) . " ms\n\n";
}

// Salva i risultati per il confronto
file_put_contents('redis_results.json', json_encode($results, JSON_PRETTY_PRINT));
echo "✅ Risultati salvati in redis_results.json\n";

Analisi dei Risultati: Redis vs MySQL

I numeri parlano chiaro. Vediamo cosa ci dicono i dati raccolti.

QuerySenza Redis (ms)Con Redis (ms)Miglioramento
Conteggio per stato45.180.5598.8%
Vendite per mese71.210.8498.8%
Top 10 clienti67.490.7698.9%
Ricerca per email57.894.1692.8%
Statistiche mensili94.641.0498.9%

Miglioramento medio: 97.6%

Redis ha ridotto i tempi di risposta dal 93% al 99%. In pratica:

  • Una query che impiegava 45ms ora ne impiega 0.55ms
  • Un report che richiedeva 95ms ora è servito in 1ms
  • Anche la ricerca per email, già veloce, è passata da 58ms a 4ms

Il miglioramento è impressionante, ma il vero valore di Redis non è solo la velocità. È ciò che non vedi:

  • Il database respira: le query pesanti non vengono più eseguite sul database
  • La concorrenza migliora: più utenti possono fare richieste contemporaneamente
  • La scalabilità è più facile: Redis può essere clusterizzato
========================================
  📊 CONFRONTO PERFORMANCE: Redis vs MySQL
========================================

Query eseguite 100 volte. I tempi sono in millisecondi (ms).

🔹 Conteggio per stato
   Senza Redis: 45.18 ms
   Con Redis:   0.55 ms
   🚀 98.8% più veloce (x82.5)

🔹 Vendite per mese
   Senza Redis: 71.21 ms
   Con Redis:   0.84 ms
   🚀 98.8% più veloce (x84.5)

🔹 Top 10 clienti
   Senza Redis: 67.49 ms
   Con Redis:   0.76 ms
   🚀 98.9% più veloce (x88.6)

🔹 Ricerca per email
   Senza Redis: 57.89 ms
   Con Redis:   4.16 ms
   🚀 92.8% più veloce (x13.9)

🔹 Statistiche mensili
   Senza Redis: 94.64 ms
   Con Redis:   1.04 ms
   🚀 98.9% più veloce (x90.7)

----------------------------------------
📈 MIGLIORAMENTO MEDIO: 97.6%

Conclusioni

Redis non è solo una moda. È uno strumento che, se usato bene, cambia radicalmente le performance di un’applicazione.

I numeri di questo benchmark parlano chiaro: miglioramento medio del 97.6%. Una query che impiegava 95ms ora risponde in 1ms. Una dashboard che caricava in mezzo secondo ora è istantanea.

Cosa ho imparato da questo test

  1. Redis è facile da installare: con Docker Compose è pronto in 5 minuti. Nessuna configurazione complicata.
  2. I benefici sono immediati: non serve riscrivere l’applicazione. Basta aggiungere un layer di cache davanti alle query lente.
  3. Il tuning non è sempre necessario: anche con la configurazione di default, Redis ha ridotto i tempi del 97%. Le ottimizzazioni avanzate servono solo se hai esigenze estreme.
  4. La cache non è per tutto: dati che cambiano in tempo reale o set enormi non vanno in Redis. Ma per dashboard, report e API è perfetto.

Quando Redis cambia davvero le carte in tavola

Redis non è solo velocità. È anche:

  • Un database che respira: le query pesanti non vengono più eseguite sul database primario. La CPU e la memoria del database rimangono libere per le operazioni critiche.
  • Una migliore esperienza utente: un’applicazione che risponde in millisecondi invece che in decine di millisecondi è percepita come più fluida e professionale.
  • Una scalabilità più semplice: Redis può essere clusterizzato orizzontalmente. Aggiungere nodi Redis è più facile che replicare un database relazionale.

Il mio consiglio pratico è di non pensare a Redis come a un’opzione, ma come a uno strumento da tenere nel cassetto.

La prossima volta che ti trovi davanti a una query lenta, chiediti: “Posso cacheare questo risultato?” Spesso la risposta è sì. E Redis è lo strumento giusto per farlo.

Inizia con qualcosa di semplice: una dashboard, un report, un’API che viene chiamata spesso. Vedrai che il miglioramento è immediato e tangibile.

 

Happy dev!