Retour à l'accueil

Verrouillage des Sessions PHP dans Redis : SessionHandler

L'article décrit les problèmes de conditions de course dans phpredis et l'implémentation de RedisSessionHandler avec mécanisme de verrouillage sur SET NX et scripts Lua. Code complet de la classe, script de test et recommandations d'optimisation en production sont fournis.

Sessions PHP dans Redis avec verrous : guide complet
Advertisement 728x90

# 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 :

Google AdInline article slot
<?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() :

Google AdInline article slot
  • 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 :

Google AdInline article slot
  • 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

Advertisement 728x90

Lire ensuite