Skip to content

Session 持久化

概述

默认情况下,PHP Session 数据存储在服务器文件系统中。为了支持分布式部署、提高性能和可靠性,可以将 Session 数据持久化到数据库(MySQL、PostgreSQL)、内存存储(Redis、Memcached)或自定义存储后端。通过实现 SessionHandlerInterface 接口可以创建自定义 Session 存储处理器。

适用场景

  • 分布式/多服务器部署
  • 高并发 Session 存储
  • Session 数据共享
  • 需要查询 Session 数据的场景

基础概念

Session 存储方式对比

存储方式速度持久性分布式适用场景
文件持久单服务器
Redis可配分布式高性能
Memcached易失分布式缓存
MySQL持久查询需求
自定义不定不定不定特殊需求

SessionHandlerInterface

php
<?php

interface SessionHandlerInterface
{
    public function open(string $path, string $name): bool;
    public function close(): bool;
    public function read(string $id): string|false;
    public function write(string $id, string $data): bool;
    public function destroy(string $id): bool;
    public function gc(int $max_lifetime): int|false;
}

PHP 7.0+ SessionHandler

PHP 7.0 引入了 SessionHandler 抽象类,PHP 8.0 进一步增强了类型声明。

语法与代码示例

Redis Session Handler

php
<?php

// 使用 phpredis 作为 Session Handler
// 安装:pecl install redis

// php.ini 配置
/*
session.save_handler = redis
session.save_path = "tcp://127.0.0.1:6379?database=2&prefix=sess_"
*/

// 或运行时配置
ini_set('session.save_handler', 'redis');
ini_set('session.save_path', 'tcp://127.0.0.1:6379?timeout=2&prefix=sess_');

session_start();
$_SESSION['user_id'] = 12345;

// Redis Session 的 TTL 由 session.gc_maxlifetime 控制
// 默认 Redis key: sess_{session_id}
// 值: 序列化的 Session 数据

Memcached Session Handler

php
<?php

// 使用 Memcached 作为 Session Handler
// 安装:pecl install memcached

ini_set('session.save_handler', 'memcached');
ini_set('session.save_path', '127.0.0.1:11211');

// 多个 Memcached 服务器
ini_set('session.save_path', 'mc1:11211,mc2:11211');

// 使用二进制协议(推荐)
ini_set('session.save_path', '127.0.0.1:11211?persistent=1&persistent_id=mysess');

session_start();

自定义数据库 Session Handler

php
<?php

declare(strict_types=1);

class DatabaseSessionHandler implements SessionHandlerInterface
{
    private PDO $pdo;
    private int $ttl;

    public function __construct(PDO $pdo, int $ttl = 7200)
    {
        $this->pdo = $pdo;
        $this->ttl = $ttl;
    }

    public function open(string $path, string $name): bool
    {
        return true;
    }

    public function close(): bool
    {
        return true;
    }

    public function read(string $id): string|false
    {
        $stmt = $this->pdo->prepare(
            'SELECT data FROM sessions WHERE id = :id AND expire_at > NOW()'
        );
        $stmt->execute(['id' => $id]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);

        return $row ? $row['data'] : '';
    }

    public function write(string $id, string $data): bool
    {
        $stmt = $this->pdo->prepare(
            'INSERT INTO sessions (id, data, expire_at)
             VALUES (:id, :data, DATE_ADD(NOW(), INTERVAL :ttl SECOND))
             ON DUPLICATE KEY UPDATE data = :data2, expire_at = DATE_ADD(NOW(), INTERVAL :ttl2 SECOND)'
        );
        return $stmt->execute([
            'id' => $id,
            'data' => $data,
            'ttl' => $this->ttl,
            'data2' => $data,
            'ttl2' => $this->ttl,
        ]);
    }

    public function destroy(string $id): bool
    {
        $stmt = $this->pdo->prepare('DELETE FROM sessions WHERE id = :id');
        return $stmt->execute(['id' => $id]);
    }

    public function gc(int $max_lifetime): int|false
    {
        $stmt = $this->pdo->prepare('DELETE FROM sessions WHERE expire_at < NOW()');
        $stmt->execute();
        return $stmt->rowCount();
    }
}

// 建表 SQL
/*
CREATE TABLE sessions (
    id VARCHAR(128) PRIMARY KEY,
    data MEDIUMTEXT NOT NULL,
    expire_at DATETIME NOT NULL,
    INDEX idx_expire (expire_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
*/

// 使用
$pdo = new PDO('mysql:host=localhost;dbname=myapp', 'user', 'pass');
$handler = new DatabaseSessionHandler($pdo, 7200);
session_set_save_handler($handler, true); // true = register_shutdown_function
session_start();

实战示例

通用 Session 管理器

php
<?php

declare(strict_types=1);

class SessionManager
{
    private static ?SessionManager $instance = null;
    private bool $started = false;

    public static function configure(array $config): SessionManager
    {
        if (self::$instance === null) {
            self::$instance = new self($config);
        }
        return self::$instance;
    }

    public static function getInstance(): ?SessionManager
    {
        return self::$instance;
    }

    private function __construct(array $config)
    {
        $this->initFromConfig($config);
        $this->start();
    }

    private function initFromConfig(array $config): void
    {
        $driver = $config['driver'] ?? 'files';

        match ($driver) {
            'redis' => $this->initRedis($config),
            'memcached' => $this->initMemcached($config),
            'database' => $this->initDatabase($config),
            default => null, // 使用默认 files
        };

        // 安全配置
        ini_set('session.cookie_httponly', '1');
        ini_set('session.cookie_secure', (string)($config['secure'] ?? true));
        ini_set('session.cookie_samesite', $config['samesite'] ?? 'Lax');
        ini_set('session.gc_maxlifetime', (string)($config['lifetime'] ?? 7200));
    }

    private function initRedis(array $config): void
    {
        ini_set('session.save_handler', 'redis');
        $host = $config['host'] ?? '127.0.0.1';
        $port = $config['port'] ?? 6379;
        $prefix = $config['prefix'] ?? 'sess_';
        $database = $config['database'] ?? 0;
        ini_set('session.save_path', "tcp://{$host}:{$port}?database={$database}&prefix={$prefix}");
    }

    private function initMemcached(array $config): void
    {
        ini_set('session.save_handler', 'memcached');
        $servers = $config['servers'] ?? ['127.0.0.1:11211'];
        ini_set('session.save_path', implode(',', $servers));
    }

    private function initDatabase(array $config): void
    {
        $pdo = new PDO(
            $config['dsn'],
            $config['username'] ?? '',
            $config['password'] ?? '',
            [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
        );
        $handler = new DatabaseSessionHandler($pdo, $config['lifetime'] ?? 7200);
        session_set_save_handler($handler, true);
    }

    private function start(): void
    {
        if ($this->started) return;
        session_start();
        $this->started = true;
    }

    public function set(string $key, mixed $value): void
    {
        $_SESSION[$key] = $value;
    }

    public function get(string $key, mixed $default = null): mixed
    {
        return $_SESSION[$key] ?? $default;
    }

    public function remove(string $key): void
    {
        unset($_SESSION[$key]);
    }

    public function destroy(): void
    {
        $_SESSION = [];
        session_regenerate_id(true);
        session_destroy();
    }
}

// 使用
$session = SessionManager::configure([
    'driver' => 'redis',
    'host' => '127.0.0.1',
    'port' => 6379,
    'prefix' => 'myapp_sess_',
    'lifetime' => 7200,
    'secure' => true,
]);

$session->set('user_id', 12345);
echo $session->get('user_id');

注意事项

session_set_save_handler 的第二个参数

php
<?php

// session_set_save_handler($handler, true)
// true 表示注册 session_write_close() 为 register_shutdown_function
// 确保 Session 数据在脚本结束时被正确写入

// 推荐:始终传 true
session_set_save_handler($handler, true);

Redis 连接故障

php
<?php

// 如果 Redis 不可用,Session 将无法工作
// 建议添加降级策略

class FailoverSessionHandler extends SessionHandlerInterface
{
    private SessionHandlerInterface $primary;
    private SessionHandlerInterface $fallback;

    public function __construct(SessionHandlerInterface $primary, SessionHandlerInterface $fallback)
    {
        $this->primary = $primary;
        $this->fallback = $fallback;
    }

    public function read(string $id): string|false
    {
        try {
            $data = $this->primary->read($id);
            if ($data !== false && $data !== '') {
                return $data;
            }
        } catch (Throwable $e) {
            error_log("Primary session handler failed: {$e->getMessage()}");
        }
        return $this->fallback->read($id);
    }

    // 实现 close、write、destroy、gc、open...
}

最佳实践

1. Redis Session 推荐配置

ini
session.save_handler = redis
session.save_path = "tcp://redis-master:6379?timeout=2.5&prefix=app_sess_&database=1"
session.cookie_httponly = 1
session.cookie_secure = 1
session.cookie_samesite = Lax
session.gc_maxlifetime = 7200

2. 自定义处理器需要序列化控制

php
<?php

// 自定义处理器需要手动处理序列化
// PHP 默认使用 session.serialize_handler = php

// php 格式:key|serialized_value
// php_binary 格式:key长度(二进制) + key + serialized_value
// php_serialize 格式:完整的 PHP serialize 格式(PHP 7.0+)

// 如果存储二进制数据,推荐 php_binary 或 php_serialize
ini_set('session.serialize_handler', 'php_serialize');

进阶用法

调试与测试技巧

php
<?php
declare(strict_types=1);

// 单元测试辅助函数
function createTestResource(): mixed
{
    return match (true) {
        default => new stdClass(),
    };
}

// 调试输出函数
function debugOutput(mixed , string  = ''): void
{
     =  ? ": " : '';
     .= print_r(, true);
    fwrite(STDERR,  . "\n");
}

// 性能基准测试
function benchmark(callable , int  = 1000): float
{
     = hrtime(true);
    for ($i = 0; $i < $iterations; $i++) {
        $fn();
    }
    return (hrtime(true) - $start) / 1e9;
}

日志记录实践

php
<?php
declare(strict_types=1);

/**
 * 简易日志记录器
 */
class SimpleLogger
{
    private string $logFile;
    private string $level = 'INFO';

    public function __construct(string $logFile)
    {
        $this->logFile = $logFile;
    }

    public function info(string $message, array $context = []): void
    {
        $this->log('INFO', $message, $context);
    }

    public function warning(string $message, array $context = []): void
    {
        $this->log('WARNING', $message, $context);
    }

    public function error(string $message, array $context = []): void
    {
        $this->log('ERROR', $message, $context);
    }

    private function log(string $level, string $message, array $context): void
    {
        $timestamp = date('Y-m-d H:i:s');
        $contextStr = $context ? ' ' . json_encode($context, JSON_UNESCAPED_UNICODE) : '';
        $line = "[{$timestamp}] [{$level}] {$message}{$contextStr}\n";
        file_put_contents($this->logFile, $line, FILE_APPEND | LOCK_EX);
    }
}

配置与环境检测

php
<?php
declare(strict_types=1);

// 环境检测工具
class EnvironmentChecker
{
    public static function checkRequirements(array $requirements): array
    {
        $results = [];
        foreach ($requirements as $name => $check) {
            $results[$name] = is_callable($check) ? $check() : false;
        }
        return $results;
    }

    public static function getSystemInfo(): array
    {
        return [
            'php_version' => PHP_VERSION,
            'os' => PHP_OS,
            'sapi' => PHP_SAPI,
            'memory_limit' => ini_get('memory_limit'),
            'max_execution_time' => ini_get('max_execution_time'),
            'loaded_extensions' => get_loaded_extensions(),
        ];
    }
}

常见问题排查

问题可能原因解决方案
连接超时网络问题/配置错误检查配置,增加超时时间
权限不足文件/目录权限使用 chmod/chown 修正
性能下降索引缺失/数据量大添加索引,优化查询
数据不一致并发冲突/事务残留使用锁机制和事务
内存溢出大数据集/未释放资源增大内存限制,分批处理

故障排除步骤

  1. 检查错误日志和异常信息
  2. 确认配置和环境是否正确
  3. 使用调试工具逐步排查
  4. 参考官方文档查找已知问题

版本兼容性说明

功能最低版本说明
基础功能PHP 8.1本文档基准版本
只读属性PHP 8.1public readonly 修饰符
枚举类型PHP 8.1enum 类型和 match 表达式
FiberPHP 8.1协程/轻量级并发
命名参数PHP 8.0foo(arg_name: value)
联合类型PHP 8.0`int
Null 安全运算符PHP 8.0$obj?->method()
析构器 promotionPHP 8.0__construct(public $x)
php
<?php
declare(strict_types=1);

// 版本兼容性检测
function ensureVersion(string $minVersion): void
{
    if (version_compare(PHP_VERSION, $minVersion, '<')) {
        throw new RuntimeException(
            sprintf('需要 PHP %s+, 当前版本: %s', $minVersion, PHP_VERSION)
        );
    }
}

ensureVersion('8.1.0');

参考链接