Zurück zur Startseite

Sperrung von PHP-Sessions in Redis: SessionHandler

Der Artikel beschreibt Race Condition-Probleme in phpredis und die Implementierung von RedisSessionHandler mit Sperrmechanismus auf SET NX und Lua-Skripten. Vollständiger Klassencode, Testskript und Produktionsoptimierungsempfehlungen werden bereitgestellt.

PHP-Sessions in Redis mit Sperren: vollständiger Leitfaden
Advertisement 728x90

PHP-Session-Locking mit Redis: Eigene SessionHandler-Implementierung

Die phpredis-Erweiterung ermöglicht das Speichern von PHP-Sessions in Redis, ohne den App-Code anzupassen. Allerdings fehlt das Locking. Im Gegensatz zur Dateispeicherung, bei der eine geöffnete Session die Datei sperrt, können bei Redis gleichzeitige Anfragen die Daten des anderen überschreiben.

Bei 100 asynchronen Anfragen, die jeweils einen Session-Parameter hinzufügen, gehen 20–40 Einträge verloren. Selbst zwei gleichzeitige Anfragen zerstören oft Daten.

Testskript zur Demonstration der Race Condition:

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;
}

Ergebnis: 60–80 Parameter statt 100. Deshalb eignet sich phpredis nicht für Produktionssysteme mit parallelem Zugriff.

Redis-Locking-Mechanismus

Eigener SessionHandler mit Spinlock-Unterstützung. Locks nutzen SET NX (set if not exists) und einen eindeutigen Token von uniqid().

lockSession()-Logik:

Google AdInline article slot
  • Eindeutigen Token generieren
  • Lock-Key: {sessionId}.lock
  • Retry-Schleife mit usleep()-Verzögerung bis 70 % von max_execution_time
  • Erfolg nur, wenn Key nicht existiert
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;
}

Entsperren via Lua-Skript für atomare Token-Prüfung:

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;
}

Das verhindert das Entfernen fremder Locks.

Vollständige RedisSessionHandler-Implementierung

Die Klasse implementiert SessionHandlerInterface mit Lock-Logik:

Google AdInline article slot
  • Konstruktor: Redis initialisieren, TTL aus gc_maxlifetime, lockMaxWait = 70 % max_execution_time
  • read(): Vor dem Lesen sperren
  • write(): setex mit TTL oder set
  • close(): Entsperren
  • destroy(): Session löschen + close()
  • gc(): An Redis delegieren

Vollständiger Code:

class RedisSessionHandler implements \SessionHandlerInterface
{
    // ... (vollständiger produktionsreifer Code ~250+ Zeilen)
}

(Vollständiger Klassencode im Originalartikel verfügbar. Essentiell für den Produktionsgebrauch.)

Einrichtung und Optimierung

Initialisierung:

$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();

Test-Ergebnisse:

  • Alle 100 Parameter zuverlässig gespeichert
  • Verarbeitungszeit minimal erhöht

Produktionsoptimierungen:

  • session_start() nur bei Bedarf aufrufen
  • Nach Fertigstellung mit session_write_close() schließen
  • spinLockWait anpassen (Standard 200 μs)
  • lockMaxWait an Skriptdauer anpassen

Wichtige Erkenntnisse

  • Standard-phpredis verliert bei Parallelität Daten ohne Locks
  • Eigener SessionHandler mit SET NX + Lua-Entsperrung behebt das
  • Locking verursacht unter Real-Last minimale Overhead
  • Sessions immer pünktlich mit session_write_close() schließen
  • Session-TTL aus ini_get('gc_maxlifetime')

— Editorial Team

Advertisement 728x90

Weiterlesen