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

PHP 自定義 Session 處理器:使用 MySQL (PDO) 實現分散式 Session 儲存

     編輯:PHP 相關知識

為什麼用資料庫儲存 Session

  • 多伺服器叢集:檔案系統無法共享 (NFS 效能差、單點故障)
  • 效能:MySQL (特別是 MEMORY/InnoDB) 可承受高併發讀寫
  • 安全:避免檔案系統權限洩露、支援加密儲存
  • 可觀測:可直接查詢線上用戶、清理過期、統計活躍度

資料表結構

CREATE TABLE `session` (
    `skey`   CHAR(32)     NOT NULL COMMENT 'session_id',
    `data`   TEXT         NULL     COMMENT '序列化後資料',
    `expire` INT UNSIGNED NOT NULL COMMENT '過期時間戳',
    PRIMARY KEY (`skey`),
    KEY `idx_expire` (`expire`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

引擎選擇:InnoDB (支援行鎖、事務) 或 MEMORY (極速、重啟清空)。生產建議 InnoDB。

完整實作 (PHP 7.4+)

<?php
declare(strict_types=1);

class PdoSessionHandler implements SessionHandlerInterface {
    private PDO $pdo;
    private int $maxLifetime;

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

    // 開啟:PDO 連線已在外部建立,這裡無需動作
    public function open(string $savePath, string $sessionName): bool {
        return true;
    }

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

    // 讀取:撈取未過期資料
    public function read(string $sessionId): string|false {
        $stmt = $this->pdo->prepare(
            'SELECT `data` FROM `session` WHERE `skey` = ? AND `expire` > ?'
        );
        $stmt->execute([$sessionId, time()]);
        $row = $stmt->fetch(PDO::FETCH_COLUMN);
        return $row ?: '';
    }

    // 寫入:UPSERT (INSERT ... ON DUPLICATE KEY UPDATE)
    public function write(string $sessionId, string $data): bool {
        $expire = time() + $this->maxLifetime;
        $stmt = $this->pdo->prepare(
            'INSERT INTO `session` (`skey`, `data`, `expire`) VALUES (?, ?, ?)
             ON DUPLICATE KEY UPDATE `data` = VALUES(`data`), `expire` = VALUES(`expire`)'
        );
        return $stmt->execute([$sessionId, $data, $expire]);
    }

    // 銷毀:登出時呼叫
    public function destroy(string $sessionId): bool {
        $stmt = $this->pdo->prepare('DELETE FROM `session` WHERE `skey` = ?');
        return $stmt->execute([$sessionId]);
    }

    // GC:定期清理過期資料
    public function gc(int $maxLifetime): int|false {
        $stmt = $this->pdo->prepare('DELETE FROM `session` WHERE `expire` < ?');
        $stmt->execute([time()]);
        return $stmt->rowCount();
    }

    // PHP 7.0+ 可選:建立新 session_id (避免衝突)
    public function create_sid(): string {
        // 使用 random_bytes 產生 256-bit 隨機值
        return bin2hex(random_bytes(16));
    }
}

// === 使用方式 ===
$pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8mb4', 'user', 'pass', [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_PERSISTENT => true, // 長連線
]);

$handler = new PdoSessionHandler($pdo, (int)ini_get('session.gc_maxlifetime'));
session_set_save_handler($handler, true); // true = 註冊 shutdown function
session_start();

// 正常使用 $_SESSION
$_SESSION['user_id'] = 123;

關鍵細節解析

方法呼叫時機注意事項
readsession_start()必須回傳字串 (空字串表示無資料),不可回傳 false 否則 PHP 視為失敗
write腳本結束 / session_write_close()資料已經 serialize() 過,直接存入
gc機率觸發 (session.gc_probability/divisor)高流量建議改用 cron 定時清理,關閉 PHP 內建 GC
create_sidsession_id() 為空時回傳唯一 ID,建議用 random_bytes

效能優化建議

  1. 持久連線:PDO::ATTR_PERSISTENT => true 減少握手開銷。
  2. 關閉 PHP 內建 GC:ini_set('session.gc_probability', 0),改用 cron 每分鐘 DELETE WHERE expire < NOW()。
  3. 壓縮資料:gzcompress(serialize($data)) 寫入,讀取時 unserialize(gzuncompress($data)),減少網傳與儲存體。
  4. 只讀複本:Read 走 Slave,Write 走 Master (需中介層支援)。

安全性加強

  • session.cookie_httponly = 1、session.cookie_secure = 1 (HTTPS)
  • session.use_strict_mode = 1 拒絕未初始化的 ID
  • 敏感資料 (權限、金額) 不要存 Session,每次查資料庫驗證
  • 定期輪換 session_id() (登入後、權限變更後)

遷移至 Redis 對照

相同介面,將 PDO 換為 Redis 客戶端,write 用 SETEX key ttl data,read 用 GET,gc 不需要 (Redis 自動過期)。效能通常優於 MySQL 10 倍以上。

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