# Verrouillage des sessions PHP dans Redis : Implémentation d'un SessionHandler personnalisé
L'extension phpredis permet de stocker les sessions PHP dans Redis sans modifier le code de votre application. Mais elle ne gère pas le verrouillage. Contrairement au stockage sur fichiers, où l'ouverture d'une session verrouille le fichier, Redis autorise les requêtes concurrentes à écraser les données les unes des autres.
Avec 100 requêtes asynchrones ajoutant chacune un paramètre de session, vous perdez 20 à 40 entrées. Même deux requêtes simultanées corrompent souvent les données.
Script de test pour démontrer la condition de course :
<?php
session_start();
$cmd = $_GET['cmd'] ?? ($_POST['cmd'] ?? '');
switch ($cmd) {
case 'result':
echo(count($_SESSION));
break;
case "set":
$_SESSION['param_' . $_POST['name']] = 1;
break;
default:
$_SESSION = [];
echo '<script src="https://code.jquery.com/jquery-1.11.3.js"></script>\n<script>\n$(document).ready(function() {\n for(var i = 0; i < 100; i++) {\n $.ajax({
type: "post",
url: "?",
dataType: "json",
data: {
name: i,
cmd: "set"
}
});\n }\n\n res = function() {\n window.location = "?cmd=result";\n }\n\n setTimeout(res, 10000);\n});\n</script>';
break;
}
Résultat : 60 à 80 paramètres au lieu de 100. Cela rend phpredis inadapté aux systèmes de production avec accès concurrent.
Mécanisme de verrouillage Redis
SessionHandler personnalisé avec support de spinlock. Les verrous utilisent SET NX (set si n'existe pas) et un jeton unique généré par uniqid().
Logique de lockSession() :
- Génération d'un jeton unique
- Clé de verrou :
{sessionId}.lock - Boucle de retry avec délai usleep() jusqu'à 70 % de max_execution_time
- Succès uniquement si la clé n'existe pas
protected function lockSession($sessionId)
{
$attempts = (1000000 * $this->lockMaxWait) / $this->spinLockWait;
$this->token = uniqid();
$this->lockKey = $sessionId . '.lock';
for ($i = 0; $i < $attempts; ++$i) {
$success = $this->redis->set(
$this->getRedisKey($this->lockKey),
$this->token,
[
'NX',
]
);
if ($success) {
$this->locked = true;
return true;
}
usleep($this->spinLockWait);
}
return false;
}
Déverrouillage via script Lua pour vérification atomique du jeton :
private function unlockSession()
{
$script = <<<LUA
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
LUA;
$this->redis->eval($script, array($this->getRedisKey($this->lockKey), $this->token), 1);
$this->locked = false;
$this->token = null;
}
Cela empêche la suppression du verrou d'un autre processus.
Implémentation complète de RedisSessionHandler
La classe implémente SessionHandlerInterface avec logique de verrouillage :
- Constructeur : Initialisation Redis, TTL depuis gc_maxlifetime, lockMaxWait = 70 % de max_execution_time
- read() : Verrou avant lecture
- write() : setex avec TTL ou set
- close() : Déverrouillage
- destroy() : Suppression de la session + close()
- gc() : Délégation à Redis
Code complet :
class RedisSessionHandler implements \SessionHandlerInterface
{
// ... (code complet prêt pour la production ~250+ lignes)
}
(Code complet de la classe disponible dans l'article original. Indispensable pour un usage en production.)
Configuration et optimisation
Initialisation :
$redis = new Redis();
if ($redis->connect('11.111.111.11', 6379) && $redis->select(0)) {
$handler = new \suffi\RedisSessionHandler\RedisSessionHandler($redis);
session_set_save_handler($handler);
}
session_start();
Résultats des tests :
- Tous les 100 paramètres sont sauvegardés de manière fiable
- Augmentation du temps de traitement minime
Optimisations en production :
- Appeler session_start() uniquement quand nécessaire
- Utiliser session_write_close() après avoir fini avec la session
- Ajuster spinLockWait (défaut 200 μs)
- Définir lockMaxWait en fonction de la durée du script
Points clés à retenir
- phpredis standard perd des données en cas de concurrence sans verrouillage
- SessionHandler personnalisé avec SET NX + déverrouillage Lua résout le problème
- Le verrouillage ajoute un surcoût minime sous charge réelle
- Toujours fermer les sessions à temps avec session_write_close()
- TTL des sessions depuis ini_get('gc_maxlifetime')
— Editorial Team
Aucun commentaire pour le moment.