預設 PHP Session 以檔案儲存,無法支援多伺服器負載均衡、難以實作單點登入、清理策略不可控。透過 session_set_save_handler() 註冊自定義儲存引擎,將 Session 寫入 MySQL/Redis 即可解決上述問題。本文提供基於 PDO 的資料庫 Session 類完整實作。
CREATE TABLE `sessions` (
`session_id` VARCHAR(128) NOT NULL PRIMARY KEY,
`session_data` TEXT NOT NULL,
`expire_time` INT UNSIGNED NOT NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
<?php
class PdoSessionHandler implements SessionHandlerInterface
{
private \PDO $pdo;
private int $lifetime;
public function __construct(\PDO $pdo, int $lifetime = 3600)
{
$this->pdo = $pdo;
$this->lifetime = $lifetime;
// 關鍵:禁止 PHP 內建檔案儲存
ini_set('session.save_handler', 'user');
session_set_save_handler($this, true);
}
/** 開啟 Session,回傳 true 表示成功 */
public function open(string $path, string $name): bool
{
return true; // PDO 連線已在建構子建立
}
/** 關閉 Session,寫入完成後自動呼叫 */
public function close(): bool
{
return true;
}
/** 讀取 Session 資料 */
public function read(string $id): string|false
{
$stmt = $this->pdo->prepare(
'SELECT session_data FROM sessions WHERE session_id = ? AND expire_time > ?'
);
$stmt->execute([$id, time()]);
$row = $stmt->fetch(\PDO::FETCH_COLUMN);
return $row !== false ? $row : '';
}
/** 寫入/更新 Session 資料 */
public function write(string $id, string $data): bool
{
$expire = time() + $this->lifetime;
$stmt = $this->pdo->prepare(
'INSERT INTO sessions (session_id, session_data, expire_time)
VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE session_data = VALUES(session_data), expire_time = VALUES(expire_time)'
);
return $stmt->execute([$id, $data, $expire]);
}
/** 銷毀 Session (登出) */
public function destroy(string $id): bool
{
$stmt = $this->pdo->prepare('DELETE FROM sessions WHERE session_id = ?');
return $stmt->execute([$id]);
}
/** 垃圾回收:清理過期資料 */
public function gc(int $maxlifetime): int|false
{
$stmt = $this->pdo->prepare('DELETE FROM sessions WHERE expire_time < ?');
$stmt->execute([time()]);
return $stmt->rowCount();
}
/** PHP 7.0+ 新介面:建立新 Session ID */
public function create_sid(): string
{
return bin2hex(random_bytes(16));
}
/** PHP 7.0+ 新介面:驗證 Session ID 格式 */
public function validateId(string $id): bool
{
return (bool)preg_match('/^[a-zA-Z0-9,\-]{22,256}$/', $id);
}
/** PHP 7.0+ 新介面:更新 Session 最後存取時間 */
public function updateTimestamp(string $id, string $data): bool
{
return $this->write($id, $data);
}
}
<?php
require_once 'PdoSessionHandler.php';
$pdo = new PDO('mysql:host=localhost;dbname=app;charset=utf8mb4', 'user', 'pass', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
// 啟動自定義處理器(必須在 session_start() 之前)
new PdoSessionHandler($pdo, 7200); // 2 小時過期
session_start();
// 正常使用 $_SESSION
$_SESSION['user_id'] = 123;
$_SESSION['username'] = 'admin';
SELECT ... FOR UPDATE 或 Redis SETNX 加鎖。DELETE FROM sessions WHERE expire_time < UNIX_TIMESTAMP()。| 現象 | 可能原因 | 解決 |
|---|---|---|
| Session 無法寫入 | 資料表欄位長度不足 / 編碼不匹配 | session_data 用 TEXT/MEDIUMTEXT,資料庫連線 charset=utf8mb4 |
| 頻繁掉線 | GC 機率過高 / lifetime 過短 | 調大 session.gc_maxlifetime,關閉隨機 GC 改用定時任務 |
| 多伺服器 Session 不共享 | 未共用同一資料庫/Redis | 確保所有節點指向同一 Session 儲存後端 |
掌握自定義 Session 處理器,是 PHP 開發者邁向分散式架構、高可用系統的必修課。