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:
- Pro e contro dell’uso di Redis come cache
- Installazione di Redis e MySQL con Docker Compose
- Creazione dei Dati Mockup per i Test
- Performance Senza Redis
- Performance Con Redis
- Analisi dei Risultati: Redis vs MySQL
- Tuning di Redis per le Massime Performance
- 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
Con Docker Compose hai Redis e MySQL pronti per i test. Nel prossimo capitolo creeremo un dataset mockup per iniziare i benchmark.
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 tabellaorders (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:
- Prima di eseguire una query, controlliamo se il risultato è già in cache
- Se sì, lo restituiamo immediatamente (senza toccare il database)
- 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.
| Query | Senza Redis (ms) | Con Redis (ms) | Miglioramento |
|---|---|---|---|
| Conteggio per stato | 45.18 | 0.55 | 98.8% |
| Vendite per mese | 71.21 | 0.84 | 98.8% |
| Top 10 clienti | 67.49 | 0.76 | 98.9% |
| Ricerca per email | 57.89 | 4.16 | 92.8% |
| Statistiche mensili | 94.64 | 1.04 | 98.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
- Redis è facile da installare: con Docker Compose è pronto in 5 minuti. Nessuna configurazione complicata.
- I benefici sono immediati: non serve riscrivere l’applicazione. Basta aggiungere un layer di cache davanti alle query lente.
- 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.
- 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!