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

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.

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

Ciclo di vita della connessione nel pool

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:

Non necessario quando:

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?