首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

PHP Session 存儲遷移 MySQL 完整方案:從文件到數據庫的高可用實踐

     編輯:PHP 綜合知識

PHP 默認文件存儲 Session 面臨小文件海量 IO、分布式共享難、統計在線用戶不便三大痛點。文章提供了一個基於 MySQL + session_set_save_handler 的完整類實現。本文將其重構為**生產級高可用方案**,覆蓋表設計優化、鎖機制、集群同步、安全加固、監控指標與現代框架整合。

一、 核心表設計優化 (MySQL 8.0 / MariaDB 10.5+)


CREATE TABLE `php_sessions` (
    `session_id`     VARBINARY(128) NOT NULL COMMENT '二進制存儲,節省空間、區分大小寫',
    `user_id`        BIGINT UNSIGNED NULL DEFAULT NULL COMMENT '關聯用戶 ID,建立索引便於踢下線/統計',
    `data`           MEDIUMBLOB NOT NULL COMMENT '序列化數據,MEDIUMBLOB 16MB 足夠',
    `created_at`     TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
    `updated_at`     TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
    `expires_at`     TIMESTAMP(6) NOT NULL COMMENT '絕對過期時間,索引加速 GC',
    `ip_address`     VARBINARY(16) NULL DEFAULT NULL COMMENT 'INET6_ATON 存儲 IPv4/IPv6',
    `user_agent`     VARCHAR(512) NULL DEFAULT NULL,
    PRIMARY KEY (`session_id`),
    INDEX `idx_user_id` (`user_id`),
    INDEX `idx_expires_at` (`expires_at`),
    INDEX `idx_updated_at` (`updated_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin
COMMENT='PHP Session 存儲表';

關鍵點:VARBINARY 存 ID (避免字符集排序開銷)、expires_at 絕對時間索引 (GC 極快)、user_id 可空 (遊客會話)、MEDIUMBLOB 避免 TEXT 字符集轉換、TIMESTAMP(6) 微秒精度防併發衝突。

二、 生產級 SessionHandler 實現 (PHP 8.1+, 實現 SessionHandlerInterface)


<?php
declare(strict_types=1);

namespace App\Session;

use SessionHandlerInterface;
use PDO;
use RuntimeException;
use Throwable;

class PdoSessionHandler implements SessionHandlerInterface
{
    private PDO $pdo;
    private int $gcProbability = 1; // 1% 概率觸發 GC (配合 session.gc_divisor=100)

    public function __construct(PDO $pdo, array $options = [])
    {
        $this->pdo = $pdo;
        $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $this->pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
        $this->gcProbability = $options['gc_probability'] ?? 1;
    }

    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 `php_sessions` WHERE `session_id` = ? AND `expires_at` > NOW(6) FOR UPDATE"
        );
        $stmt->execute([$id]);
        $row = $stmt->fetch(PDO::FETCH_COLUMN);
        return $row !== false ? $row : '';
    }

    public function write(string $id, string $data): bool
    {
        $expiresAt = (new DateTime())->modify("+{$this->getLifetime()} seconds")->format('Y-m-d H:i:s.u');
        $userId = $this->extractUserId($data); // 從序列化數據解析 user_id (可選)

        $sql = "INSERT INTO `php_sessions` (`session_id`, `user_id`, `data`, `expires_at`, `ip_address`, `user_agent`)
                VALUES (?, ?, ?, ?, INET6_ATON(?), ?)
                ON DUPLICATE KEY UPDATE
                    `user_id` = VALUES(`user_id`),
                    `data` = VALUES(`data`),
                    `expires_at` = VALUES(`expires_at`),
                    `updated_at` = NOW(6),
                    `ip_address` = VALUES(`ip_address`),
                    `user_agent` = VALUES(`user_agent`)";

        $stmt = $this->pdo->prepare($sql);
        return $stmt->execute([
            $id,
            $userId,
            $data,
            $expiresAt,
            $_SERVER['REMOTE_ADDR'] ?? null,
            $_SERVER['HTTP_USER_AGENT'] ?? null,
        ]);
    }

    public function destroy(string $id): bool
    {
        $stmt = $this->pdo->prepare("DELETE FROM `php_sessions` WHERE `session_id` = ?");
        return $stmt->execute([$id]);
    }

    public function gc(int $maxLifetime): int|false
    {
        // 僅按概率執行,避免每請求掃表
        if (mt_rand(1, 100) > $this->gcProbability) return 0;

        $stmt = $this->pdo->prepare("DELETE FROM `php_sessions` WHERE `expires_at` <= NOW(6)");
        $stmt->execute();
        return $stmt->rowCount();
    }

    private function getLifetime(): int
    {
        return (int) ini_get('session.gc_maxlifetime');
    }

    private function extractUserId(string $data): ?int
    {
        // 簡易解析 PHP 序列化格式: user_id|i:123;
        if (preg_match('/user_id\|i:(\d+);/', $data, $m)) {
            return (int) $m[1];
        }
        return null;
    }

    // === 業務擴展方法 ===
    public function getByUserId(int $userId): array
    {
        $stmt = $this->pdo->prepare("SELECT * FROM `php_sessions` WHERE `user_id` = ? AND `expires_at` > NOW(6)");
        $stmt->execute([$userId]);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    public function revokeUserSessions(int $userId, ?string $exceptId = null): int
    {
        $sql = "DELETE FROM `php_sessions` WHERE `user_id` = ?";
        $params = [$userId];
        if ($exceptId) {
            $sql .= " AND `session_id` != ?";
            $params[] = $exceptId;
        }
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($params);
        return $stmt->rowCount();
    }
}

三、 註冊與配置 (啟動文件最早處)


<?php
// bootstrap/session.php
use App\Session\PdoSessionHandler;

$pdo = new PDO(
    "mysql:host=127.0.0.1;dbname=app;charset=utf8mb4",
    'session_user', 'strong_pass',
    [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);

$handler = new PdoSessionHandler($pdo, ['gc_probability' => 5]); // 5% GC 概率

// 關鍵:必須在 session_start() 之前註冊
session_set_save_handler($handler, true); // true = 使用 OOP 介面
session_start();

四、 高可用與集群方案

架構Session 一致性延遲成本推薦場景
單主 MySQL + 讀寫分離強一致 (寫主讀從)低中中小型、讀多寫少
MySQL Group Replication / InnoDB Cluster強一致 (多主同步)中高金融、核心業務
Redis Cluster (主流)最終一致 (異步複製)極低中高併發、微服務、雲原生
Redis + MySQL 雙寫 (雙活)強一致 (MySQL 兜底)低高零容忍丟失、合規要求

現代首選:Redis Cluster (phpredis + session.save_handler = redis),配合 Lua 腳本實現原子鎖、Lua GC、Pipeline 批量操作。MySQL 方案適合「已有 MySQL、不想引入 Redis、數據量 < 1000 萬、需 SQL 查詢分析」場景。

五、 安全加固清單

  1. Session ID 熵值:session.sid_length = 48, session.sid_bits_per_character = 6 (PHP 7.1+)。
  2. Cookie 屬性:session.cookie_httponly=1, session.cookie_secure=1 (HTTPS), session.cookie_samesite=Strict (或 Lax)。
  3. 防固定攻擊:session.use_strict_mode=1, session.use_only_cookies=1。
  4. 數據加密:敏感數據寫入 Session 前應用層加密 (AES-GCM),或使用 session.serialize_handler = php_serialize + 自定義加密序列化器。
  5. IP/UA 綁定:讀取時驗證 ip_address/user_agent 一致,不一致銷毀會話 (需權衡移動端體驗)。
  6. 定期輪換 ID:權限變更 (登入/提權/修改密碼) 時調用 session_regenerate_id(true)。

六、 監控指標 (Prometheus Exporter)


# HELP php_sessions_active 當前活躍會話數
# TYPE php_sessions_active gauge
php_sessions_active 12345

# HELP php_sessions_gc_deleted_last GC 最後一次清理數量
# TYPE php_sessions_gc_deleted_last gauge
php_sessions_gc_deleted_last 37

# HELP php_sessions_write_latency_seconds 寫入延遲
# TYPE php_sessions_write_latency_seconds histogram
php_sessions_write_latency_seconds_bucket{le="0.005"} 1200
php_sessions_write_latency_seconds_bucket{le="0.01"} 1250
...

七、 總結

將 Session 遷移到 MySQL 解決了文件存儲的三大痛點,但引入了數據庫連接開銷、鎖競爭、單點故障新挑戰。本文提供的 PdoSessionHandler 通過 FOR UPDATE 行鎖、ON DUPLICATE KEY UPDATE 原子寫入、概率 GC、expires_at 索引、預處理語句、IP/UA 記錄 等技術,構建了生產級可用方案。對於高併發/微服務場景,**強烈建議直接上 Redis Cluster**;MySQL 方案作為「不引入新組件、需 SQL 審計、數據量可控」的優秀備選。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved