Back to Home

Locking PHP Sessions in Redis: SessionHandler

The article describes race condition problems in phpredis and implementation of RedisSessionHandler with locking mechanism on SET NX and Lua scripts. Full class code, test script, and production optimization recommendations are provided.

PHP sessions in Redis with locks: full guide
Advertisement 728x90

PHP Session Locking in Redis: Custom SessionHandler Implementation

The phpredis extension lets you store PHP sessions in Redis without changing your app code. But it lacks locking. Unlike file storage, where an open session locks the file, Redis lets concurrent requests overwrite each other's data.

With 100 async requests each adding a session parameter, you lose 20–40 entries. Even two simultaneous requests often clobber data.

Test script to demo the 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;
}

Result: 60–80 parameters instead of 100. This makes phpredis unsuitable for production systems with concurrent access.

Redis Locking Mechanism

Custom SessionHandler with spinlock support. Locks use SET NX (set if not exists) and a unique token from uniqid().

lockSession() logic:

Google AdInline article slot
  • Generate unique token
  • Lock key: {sessionId}.lock
  • Retry loop with usleep() delay up to 70% of max_execution_time
  • Success only if key doesn't exist
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;
}

Unlock via Lua script for atomic token check:

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

This prevents removing someone else's lock.

Full RedisSessionHandler Implementation

Class implements SessionHandlerInterface with lock logic:

Google AdInline article slot
  • Constructor: Init Redis, TTL from gc_maxlifetime, lockMaxWait = 70% max_execution_time
  • read(): Lock before reading
  • write(): setex with TTL or set
  • close(): Unlock
  • destroy(): Delete session + close()
  • gc(): Delegate to Redis

Full code:

class RedisSessionHandler implements \SessionHandlerInterface
{
    // ... (full production-ready code ~250+ lines)
}

(Complete class code available in original article. Essential for production use.)

Setup and Optimization

Initialization:

$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 results:

  • All 100 parameters saved reliably
  • Processing time increase minimal

Production optimizations:

  • Call session_start() only when needed
  • Use session_write_close() after finishing with session
  • Tune spinLockWait (default 200μs)
  • Set lockMaxWait based on script duration

Key Takeaways

  • Standard phpredis loses data under concurrency without locks
  • Custom SessionHandler with SET NX + Lua unlock fixes it
  • Locking adds minimal overhead under real load
  • Always close sessions timely with session_write_close()
  • Session TTL from ini_get('gc_maxlifetime')

— Editorial Team

Advertisement 728x90

Read Next