Blokování PHP session v Redis: Implementace SessionHandler
Rozšíření phpredis umožňuje ukládat PHP session do Redis bez změn v kódu aplikace. Nicméně nepodporuje mechanismus blokování. Na rozdíl od souborového úložiště, kde otevřená session blokuje soubor, v Redis paralelní požadavky přepisují data navzájem.
Při 100 asynchronních požadavcích, z nichž každý přidává parametr do session, dochází ke ztrátě 20–40 záznamů. I při dvou souběžných požadavcích se data často přepisují.
Testovací skript pro demonstraci 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({\n type: "post",\n url: "?",\n dataType: "json",\n data: {\n name: i,\n cmd: "set"\n }\n });\n }\n\n res = function() {\n window.location = "?cmd=result";\n }\n\n setTimeout(res, 10000);\n});\n</script>';
break;
}
Výsledek: 60–80 parametrů místo 100. To činí phpredis nevhodným pro produkční systémy s konkurenčním přístupem.
Mechanismus blokování v Redis
Implementace vlastního SessionHandler s podporou spinlocku. Blokování se nastavuje pomocí SET NX (set if not exists) a unikátního tokenu na základě uniqid().
Logika lockSession():
- Generování unikátního tokenu
- Klíč blokování:
{sessionId}.lock - Smyčka pokusů se zpožděním usleep() do 70 % z max_execution_time
- Úspěch pouze při nepřítomnosti klíče
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;
}
Odblokování přes Lua skript pro atomickou kontrolu tokenu:
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;
}
Toto zabraňuje odstranění cizího blokování.
Kompletní implementace RedisSessionHandler
Třída implementuje SessionHandlerInterface s přidanou logikou blokování:
- Konstruktor: Inicializace Redis, TTL z gc_maxlifetime, lockMaxWait = 70 % max_execution_time
- read(): Blokování před čtením
- write(): setex s TTL nebo set
- close(): Odblokování
- destroy(): Odstranění session + close()
- gc(): Delegováno na Redis
Kompletní kód:
class RedisSessionHandler implements \SessionHandlerInterface
{
// ... (kompletní kód jako v originále, 250+ řádků)
}
(Kód třídy je uveden kompletně v originálním článku. Zde je vynechán pro stručnost, ale nutný pro produkci.)
Připojení a optimalizace
Inicializace:
$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();
Výsledky testování:
- 100 parametrů se zaručeně uloží
- Čas zpracování vzrostl jen mírně
Optimalizace pro produkci:
- Volat session_start() pouze při potřebě
- session_write_close() po dokončení práce se session
- Nastavovat spinLockWait (výchozí 200 μs)
- lockMaxWait podle délky skriptu
Důležité body
- Standardní phpredis ztrácí data při souběžnosti kvůli absenci blokování
- Vlastní SessionHandler s SET NX + Lua odblokování řeší problém
- Blokování přidává minimální overhead při reálné zátěži
- Nutné včas zavřít session pomocí session_write_close()
- TTL session se bere z ini_get('gc_maxlifetime')
— Editorial Team
Zatím žádné komentáře.