PDO Pool: Pool di Connessioni al Database
Il Problema
Quando si lavora con le coroutine, sorge il problema della condivisione dei descrittori di I/O. Se lo stesso socket viene usato da due coroutine che contemporaneamente scrivono o leggono pacchetti diversi da esso, i dati si mescolano e il risultato è imprevedibile. Pertanto, non puoi semplicemente usare lo stesso oggetto PDO in diverse coroutine!
D'altra parte, creare una connessione separata per ogni coroutine è una strategia molto dispendiosa. Annulla i vantaggi dell'I/O concorrente. Pertanto, tipicamente si usano pool di connessioni per interagire con API esterne, database e altre risorse.
$pdo = new PDO('mysql:host=localhost;dbname=app', 'root', 'secret');
// Dieci coroutine usano contemporaneamente lo stesso $pdo
for ($i = 0; $i < 10; $i++) {
spawn(function() use ($pdo, $i) {
$pdo->beginTransaction();
$pdo->exec("INSERT INTO orders (user_id) VALUES ($i)");
// Un'altra coroutine ha già chiamato COMMIT su questa stessa connessione!
$pdo->commit(); // Caos
});
}Potresti creare una connessione separata in ogni coroutine, ma con mille coroutine otterresti mille connessioni TCP. MySQL permette 151 connessioni simultanee per impostazione predefinita. PostgreSQL -- 100.
La Soluzione: PDO Pool
PDO Pool -- un pool di connessioni al database integrato nel core di PHP. Dà automaticamente a ogni coroutine la propria connessione da un set pre-preparato e la restituisce quando la coroutine ha finito di lavorare.
$pdo = new PDO('mysql:host=localhost;dbname=app', 'root', 'secret', [
PDO::ATTR_POOL_ENABLED => true,
PDO::ATTR_POOL_MIN => 2,
PDO::ATTR_POOL_MAX => 10,
]);
// Dieci coroutine -- ognuna ottiene la propria connessione
for ($i = 0; $i < 10; $i++) {
spawn(function() use ($pdo, $i) {
// Il pool alloca automaticamente una connessione per questa coroutine
$pdo->beginTransaction();
$pdo->exec("INSERT INTO orders (user_id) VALUES ($i)");
$pdo->commit();
// La connessione viene restituita al pool
});
}Dall'esterno, il codice appare come se si stesse lavorando con un normale PDO. Il pool è completamente trasparente.
Come Abilitarlo
Il pool viene abilitato tramite gli attributi del costruttore PDO:
$pdo = new PDO($dsn, $user, $password, [
PDO::ATTR_POOL_ENABLED => true, // Abilita il pool
PDO::ATTR_POOL_MIN => 0, // Connessioni minime (predefinito 0)
PDO::ATTR_POOL_MAX => 10, // Connessioni massime (predefinito 10)
PDO::ATTR_POOL_HEALTHCHECK_INTERVAL => 30, // Intervallo di health check (sec, 0 = disabilitato)
]);| Attributo | Significato | Predefinito |
|---|---|---|
POOL_ENABLED | Abilita il pool | false |
POOL_MIN | Numero minimo di connessioni mantenute aperte dal pool | 0 |
POOL_MAX | Numero massimo di connessioni simultanee | 10 |
POOL_HEALTHCHECK_INTERVAL | Quanto spesso verificare che una connessione sia attiva (in secondi) | 0 |
POOL_STMT_CACHE_SIZE | Dimensione della cache di prepared statement per singola connessione fisica | 0 (off) |
Associazione delle Connessioni alle Coroutine
Ogni coroutine ottiene la propria connessione dal pool. Tutte le chiamate a query(), exec(), prepare() all'interno di una singola coroutine passano attraverso la stessa connessione.
$pdo = new PDO($dsn, $user, $password, [
PDO::ATTR_POOL_ENABLED => true,
PDO::ATTR_POOL_MAX => 5,
]);
$coro1 = spawn(function() use ($pdo) {
// Tutte e tre le query passano attraverso la connessione #1
$pdo->query("SELECT 1");
$pdo->query("SELECT 2");
$pdo->query("SELECT 3");
// Coroutine terminata -- la connessione #1 torna al pool
});
$coro2 = spawn(function() use ($pdo) {
// Tutte le query passano attraverso la connessione #2
$pdo->query("SELECT 4");
// Coroutine terminata -- la connessione #2 torna al pool
});Se una coroutine non sta più usando la connessione (nessuna transazione o statement attivo), il pool potrebbe restituirla prima -- senza attendere la fine della coroutine.
Cache di prepared statement
Si attiva con l'attributo PDO::ATTR_POOL_STMT_CACHE_SIZE => N alla creazione del PDO. Il pool mantiene per ogni connessione fisica una cache LRU degli ultimi N statement preparati. Quando una coroutine richiama prepare() con lo stesso SQL, il pool restituisce uno statement lato server già preparato — senza round-trip al DB.
$pdo = new PDO($dsn, $user, $password, [
PDO::ATTR_POOL_ENABLED => true,
PDO::ATTR_POOL_MAX => 10,
PDO::ATTR_POOL_STMT_CACHE_SIZE => 64, // fino a 64 stmt per connessione
]);
spawn(function () use ($pdo) {
for ($i = 0; $i < 1000; $i++) {
// Prima chiamata: PREPARE reale sul server.
// Tutte le successive sulla stessa connessione: cache hit, zero traffico di rete.
$stmt = $pdo->prepare('SELECT name FROM users WHERE id = ?');
$stmt->execute([$i]);
$row = $stmt->fetch();
}
});Su un ciclo stretto prepare → execute → fetch questo porta circa a ~2.9× di accelerazione (dipende dal driver e dal carico).
Driver supportati
pdo_pgsql, pdo_mysql, pdo_sqlite.
Quando la cache non si applica
La cache viene saltata automaticamente nei seguenti casi, per non alterare la semantica:
PDO_CURSOR_SCROLL— un cursore lato server di un result set scrollabile non si può riusare.PDO::ATTR_EMULATE_PREPARES = true— le query emulate non hanno uno statement lato server.PGSQL_ATTR_DISABLE_PREPARES— rifiuto esplicito del prepare nel driver PG.
Invalidazione della cache per cambi di schema / piano
Se lo schema di una tabella cambia (ALTER TABLE), il piano lato server di uno statement vecchio può non essere più valido. Il pool riconosce questi errori e riesegue trasparentemente la query: lo statement vecchio viene rimosso dalla cache, ne viene fatto un nuovo prepare e il codice utente riceve un risultato positivo al primo tentativo.
| Driver | Codici di errore che innescano il retry |
|---|---|
| PostgreSQL | SQLSTATE 0A000 (feature not supported, cached plan must not change result type), 26000 (invalid SQL statement name) |
| MySQL | 1243 (unknown prepared statement handler), 1615 (prepared statement needs to be re-prepared), 2057 (statement has wrong column count) |
Quanto mettere
L'LRU lavora in modo indipendente per ogni connessione fisica, quindi il consumo di memoria totale sul server DB è ≈ POOL_MAX × POOL_STMT_CACHE_SIZE statement preparati in un dato momento.
Valori ragionevoli:
- applicazione web con qualche decina di SQL unici:
16..32; - servizio con molte query diverse:
64..256; - se le query sono quasi sempre uniche, la cache è inutile: lascia
0.
Transazioni
Le transazioni funzionano come nel PDO normale. Ma il pool garantisce che finché una transazione è attiva, la connessione è vincolata alla coroutine e non tornerà al pool.
spawn(function() use ($pdo) {
$pdo->beginTransaction();
$pdo->exec("UPDATE accounts SET balance = balance - 100 WHERE id = 1");
$pdo->exec("UPDATE accounts SET balance = balance + 100 WHERE id = 2");
$pdo->commit();
// Solo dopo il commit la connessione può tornare al pool
});Rollback Automatico
Se una coroutine termina senza chiamare commit(), il pool esegue automaticamente il rollback della transazione prima di restituire la connessione al pool. Questa è una protezione contro la perdita accidentale di dati.
spawn(function() use ($pdo) {
$pdo->beginTransaction();
$pdo->exec("DELETE FROM users WHERE id = 1");
// Dimenticato commit()
// Coroutine terminata -- il pool chiamerà ROLLBACK automaticamente
});Ciclo di Vita della Connessione
Un diagramma tecnico dettagliato con le chiamate interne si trova in Architettura del PDO Pool.
Accesso all'Oggetto Pool
Il metodo getPool() restituisce l'oggetto Async\Pool attraverso il quale puoi ottenere statistiche:
$pool = $pdo->getPool();
if ($pool !== null) {
echo "Pool attivo: " . get_class($pool) . "\n"; // Async\Pool
}Se il pool non è abilitato, getPool() restituisce null.
Quando Usarlo
Usa PDO Pool quando:
- L'applicazione funziona in modalità asincrona con TrueAsync
- Più coroutine accedono contemporaneamente al database
- Devi limitare il numero di connessioni al database
Non necessario quando:
- L'applicazione è sincrona (PHP classico)
- Solo una coroutine lavora con il database
- Vengono usate connessioni persistenti (sono incompatibili con il pool)
Driver Supportati
| Driver | Supporto Pool |
|---|---|
pdo_mysql | Si |
pdo_pgsql | Si |
pdo_sqlite | Si |
pdo_odbc | No |
Gestione degli Errori
Se il pool non riesce a creare una connessione (credenziali errate, server non disponibile), l'eccezione viene propagata alla coroutine che ha richiesto la connessione:
$pdo = new PDO('mysql:host=localhost;dbname=app', 'root', 'wrong_password', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_POOL_ENABLED => true,
PDO::ATTR_POOL_MIN => 0,
]);
spawn(function() use ($pdo) {
try {
$pdo->query("SELECT 1");
} catch (PDOException $e) {
echo "Connessione fallita: " . $e->getMessage() . "\n";
}
});Nota POOL_MIN => 0: se imposti il minimo superiore a zero, il pool tenterà di creare le connessioni in anticipo, e l'errore si verificherà alla creazione dell'oggetto PDO.
Esempio Reale: Elaborazione Parallela degli Ordini
use function Async\spawn;
use function Async\await;
$pdo = new PDO('mysql:host=localhost;dbname=shop', 'app', 'secret', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_POOL_ENABLED => true,
PDO::ATTR_POOL_MIN => 2,
PDO::ATTR_POOL_MAX => 5,
]);
// Ottieni la lista degli ordini da elaborare
$orders = [101, 102, 103, 104, 105, 106, 107, 108, 109, 110];
$coroutines = [];
foreach ($orders as $orderId) {
$coroutines[] = spawn(function() use ($pdo, $orderId) {
// Ogni coroutine ottiene la propria connessione dal pool
$pdo->beginTransaction();
$stmt = $pdo->prepare("SELECT * FROM orders WHERE id = ? FOR UPDATE");
$stmt->execute([$orderId]);
$order = $stmt->fetch();
if ($order['status'] === 'pending') {
$pdo->exec("UPDATE orders SET status = 'processing' WHERE id = $orderId");
$pdo->exec("INSERT INTO order_log (order_id, action) VALUES ($orderId, 'started')");
}
$pdo->commit();
return $orderId;
});
}
// Attendi il completamento di tutte le coroutine
foreach ($coroutines as $coro) {
$processedId = await($coro);
echo "Ordine #$processedId elaborato\n";
}Dieci ordini vengono elaborati concorrentemente, ma attraverso un massimo di cinque connessioni al database. Ogni transazione è isolata. Le connessioni vengono riutilizzate tra le coroutine.
Cosa Leggere Dopo?
- Demo Interattiva PDO Pool -- una dimostrazione visiva del funzionamento del pool di connessioni
- Architettura PDO Pool -- internals del pool, diagrammi, ciclo di vita delle connessioni
- Coroutine -- come funzionano le coroutine
- Scope -- gestione di gruppi di coroutine
- spawn() -- lancio delle coroutine
- await() -- attesa dei risultati