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:
<?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:
- 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:
- 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
Noch keine Kommentare.