PDO Pool : pool de connexions base de donnees
Le probleme
Lorsqu'on travaille avec des coroutines, le probleme du partage des descripteurs d'E/S se pose. Si le meme socket est utilise par deux coroutines qui ecrivent ou lisent simultanement des paquets differents, les donnees vont se melanger et le resultat sera imprevisible. Par consequent, vous ne pouvez pas simplement utiliser le meme objet PDO dans differentes coroutines !
D'un autre cote, creer une connexion separee pour chaque coroutine a chaque fois est une strategie tres couteuse. Cela annule les avantages des E/S concurrentes. C'est pourquoi les pools de connexions sont generalement utilises pour interagir avec les API externes, les bases de donnees et autres ressources.
$pdo = new PDO('mysql:host=localhost;dbname=app', 'root', 'secret');
// Dix coroutines utilisent simultanement le meme $pdo
for ($i = 0; $i < 10; $i++) {
spawn(function() use ($pdo, $i) {
$pdo->beginTransaction();
$pdo->exec("INSERT INTO orders (user_id) VALUES ($i)");
// Une autre coroutine a deja appele COMMIT sur cette meme connexion !
$pdo->commit(); // Chaos
});
}Vous pourriez creer une connexion separee dans chaque coroutine, mais alors avec mille coroutines vous auriez mille connexions TCP. MySQL autorise 151 connexions simultanees par defaut. PostgreSQL -- 100.
La solution : PDO Pool
PDO Pool -- un pool de connexions base de donnees integre au coeur de PHP. Il donne automatiquement a chaque coroutine sa propre connexion depuis un ensemble pre-prepare et la restitue lorsque la coroutine a termine son travail.
$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,
]);
// Dix coroutines -- chacune obtient sa propre connexion
for ($i = 0; $i < 10; $i++) {
spawn(function() use ($pdo, $i) {
// Le pool alloue automatiquement une connexion pour cette coroutine
$pdo->beginTransaction();
$pdo->exec("INSERT INTO orders (user_id) VALUES ($i)");
$pdo->commit();
// La connexion est restituee au pool
});
}De l'exterieur, le code ressemble a un travail avec un PDO ordinaire. Le pool est completement transparent.
Comment l'activer
Le pool est active via les attributs du constructeur PDO :
$pdo = new PDO($dsn, $user, $password, [
PDO::ATTR_POOL_ENABLED => true, // Activer le pool
PDO::ATTR_POOL_MIN => 0, // Connexions minimales (defaut 0)
PDO::ATTR_POOL_MAX => 10, // Connexions maximales (defaut 10)
PDO::ATTR_POOL_HEALTHCHECK_INTERVAL => 30, // Intervalle de verification de sante (sec, 0 = desactive)
]);| Attribut | Signification | Defaut |
|---|---|---|
POOL_ENABLED | Activer le pool | false |
POOL_MIN | Nombre minimum de connexions maintenues ouvertes par le pool | 0 |
POOL_MAX | Nombre maximum de connexions simultanees | 10 |
POOL_HEALTHCHECK_INTERVAL | Frequence de verification que la connexion est active (en secondes) | 0 |
POOL_STMT_CACHE_SIZE | Taille du cache de prepared statements par connexion physique | 0 (off) |
Liaison des connexions aux coroutines
Chaque coroutine obtient sa propre connexion du pool. Tous les appels a query(), exec(), prepare() au sein d'une meme coroutine passent par la meme connexion.
$pdo = new PDO($dsn, $user, $password, [
PDO::ATTR_POOL_ENABLED => true,
PDO::ATTR_POOL_MAX => 5,
]);
$coro1 = spawn(function() use ($pdo) {
// Les trois requetes passent par la connexion #1
$pdo->query("SELECT 1");
$pdo->query("SELECT 2");
$pdo->query("SELECT 3");
// Coroutine terminee -- la connexion #1 retourne au pool
});
$coro2 = spawn(function() use ($pdo) {
// Toutes les requetes passent par la connexion #2
$pdo->query("SELECT 4");
// Coroutine terminee -- la connexion #2 retourne au pool
});Si une coroutine n'utilise plus la connexion (pas de transactions ou de requetes actives), le pool peut la restituer plus tot -- sans attendre la fin de la coroutine.
Cache de prepared statements
Activé par l'attribut PDO::ATTR_POOL_STMT_CACHE_SIZE => N lors de la création du PDO. Le pool maintient par connexion physique un cache LRU des N derniers prepared statements. Lorsqu'une coroutine refait prepare() avec le même SQL, le pool renvoie le statement côté serveur déjà préparé — sans round-trip vers la BD.
$pdo = new PDO($dsn, $user, $password, [
PDO::ATTR_POOL_ENABLED => true,
PDO::ATTR_POOL_MAX => 10,
PDO::ATTR_POOL_STMT_CACHE_SIZE => 64, // jusqu'à 64 stmt par connexion
]);
spawn(function () use ($pdo) {
for ($i = 0; $i < 1000; $i++) {
// Premier appel : véritable PREPARE côté serveur.
// Tous les suivants sur cette connexion : hit cache, trafic réseau nul.
$stmt = $pdo->prepare('SELECT name FROM users WHERE id = ?');
$stmt->execute([$i]);
$row = $stmt->fetch();
}
});Sur une boucle serrée prepare → execute → fetch, cela donne un gain d'environ 2,9× (dépend du driver et de la charge).
Drivers supportés
pdo_pgsql, pdo_mysql, pdo_sqlite.
Quand le cache ne s'applique pas
Le cache est automatiquement contourné dans les cas suivants, pour ne pas casser la sémantique :
PDO_CURSOR_SCROLL: un curseur serveur sur un résultat scrollable n'est pas réutilisable.PDO::ATTR_EMULATE_PREPARES = true: les requêtes émulées n'ont pas de statement côté serveur.PGSQL_ATTR_DISABLE_PREPARES: refus explicite du prepare côté driver PG.
Invalidation du cache au changement de schéma / de plan
Si le schéma d'une table change (ALTER TABLE), le plan serveur d'un ancien stmt peut devenir invalide. Le pool reconnaît ces erreurs et réexécute la requête de façon transparente : l'ancien stmt est éjecté du cache, un nouveau prepare est fait, et le code utilisateur reçoit un résultat correct dès la première tentative.
| Driver | Codes d'erreur déclenchant le 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) |
Quelle valeur choisir
Le LRU fonctionne indépendamment sur chaque connexion physique, donc la consommation mémoire totale côté serveur BD est de l'ordre de POOL_MAX × POOL_STMT_CACHE_SIZE stmts préparés simultanément.
Valeurs raisonnables :
- application web avec quelques dizaines de SQL uniques :
16..32; - service avec beaucoup de requêtes différentes :
64..256; - si en pratique le SQL est presque toujours unique, le cache est inutile, laissez
0.
Transactions
Les transactions fonctionnent de la meme maniere que dans PDO ordinaire. Mais le pool garantit que tant qu'une transaction est active, la connexion est epinglee a la coroutine et ne retournera pas au 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();
// Ce n'est qu'apres le commit que la connexion peut retourner au pool
});Rollback automatique
Si une coroutine se termine sans appeler commit(), le pool annule automatiquement la transaction avant de restituer la connexion au pool. C'est une protection contre la perte accidentelle de donnees.
spawn(function() use ($pdo) {
$pdo->beginTransaction();
$pdo->exec("DELETE FROM users WHERE id = 1");
// Oubli de commit()
// Coroutine terminee -- le pool appellera automatiquement ROLLBACK
});Cycle de vie d'une connexion
Un diagramme technique detaille avec les appels internes se trouve dans l'architecture du PDO Pool.
Acces a l'objet Pool
La methode getPool() retourne l'objet Async\Pool a travers lequel vous pouvez obtenir des statistiques :
$pool = $pdo->getPool();
if ($pool !== null) {
echo "Pool actif : " . get_class($pool) . "\n"; // Async\Pool
}Si le pool n'est pas active, getPool() retourne null.
Quand l'utiliser
Utilisez PDO Pool quand :
- L'application s'execute en mode asynchrone avec TrueAsync
- Plusieurs coroutines accedent simultanement a la base de donnees
- Vous devez limiter le nombre de connexions a la base de donnees
Pas necessaire quand :
- L'application est synchrone (PHP classique)
- Une seule coroutine travaille avec la base de donnees
- Les connexions persistantes sont utilisees (elles sont incompatibles avec le pool)
Pilotes supportes
| Pilote | Support du pool |
|---|---|
pdo_mysql | Oui |
pdo_pgsql | Oui |
pdo_sqlite | Oui |
pdo_odbc | Non |
Gestion des erreurs
Si le pool ne peut pas creer une connexion (mauvais identifiants, serveur indisponible), l'exception est propagee a la coroutine qui a demande la connexion :
$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 "Echec de connexion : " . $e->getMessage() . "\n";
}
});Notez POOL_MIN => 0 : si vous definissez le minimum superieur a zero, le pool essaiera de creer des connexions a l'avance, et l'erreur se produira lors de la creation de l'objet PDO.
Exemple concret : traitement parallele de commandes
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,
]);
// Obtenir la liste des commandes a traiter
$orders = [101, 102, 103, 104, 105, 106, 107, 108, 109, 110];
$coroutines = [];
foreach ($orders as $orderId) {
$coroutines[] = spawn(function() use ($pdo, $orderId) {
// Chaque coroutine obtient sa propre connexion du 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;
});
}
// Attendre la fin de toutes les coroutines
foreach ($coroutines as $coro) {
$processedId = await($coro);
echo "Commande #$processedId traitee\n";
}Dix commandes sont traitees en concurrence, mais a travers un maximum de cinq connexions a la base de donnees. Chaque transaction est isolee. Les connexions sont reutilisees entre les coroutines.
Et ensuite ?
- Demo interactive du PDO Pool -- une demonstration visuelle du fonctionnement du pool de connexions
- Architecture du PDO Pool -- details internes du pool, diagrammes, cycle de vie des connexions
- Coroutines -- comment fonctionnent les coroutines
- Scope -- gestion de groupes de coroutines
- spawn() -- lancement de coroutines
- await() -- attente de resultats