默認文件存儲 Session 面臨:多機負載均衡無法共享、大量小文件 Inode 耗盡、I/O 瓶頸、清理機制不可控。將 Session 遷移至 MySQL(或 Redis)是分布式應用的標準解法。本文基於 PHP session_set_save_handler() 實現完整 MySQL 存儲處理器,含表設計、六大回調函數、GC 機制、併發安全、性能優化要點。
CREATE TABLE `php_session` (
`session_id` VARCHAR(128) NOT NULL PRIMARY KEY COMMENT 'PHPSESSID',
`session_data` TEXT NOT NULL COMMENT '序列化後的 $_SESSION',
`session_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最後訪問時間',
`expire_time` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '過期時間戳,冗余字段加速 GC'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='PHP Session 存儲表';
索引策略:主鍵 session_id 滿足點查;expire_time 建普通索引加速 DELETE WHERE expire_time < NOW()。
<?php
class MySqlSessionHandler implements SessionHandlerInterface {
private $pdo;
private $table = 'php_session';
private $gcProbability = 1;
private $gcDivisor = 100; // 1% 請求觸發 GC
private $gcMaxLifetime = 1440; // 24 分鐘
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
// 1. open: 初始化連接(這裡已在構造函數完成)
public function open($savePath, $sessionName): bool { return true; }
// 2. close: 關閉連接
public function close(): bool { return true; }
// 3. read: 讀取會話數據
public function read($sessionId): string {
$stmt = $this->pdo->prepare("SELECT session_data FROM {$this->table} WHERE session_id = ? AND expire_time > UNIX_TIMESTAMP()");
$stmt->execute([$sessionId]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row ? $row['session_data'] : '';
}
// 4. write: 寫入/更新會話數據(UPSERT)
public function write($sessionId, $data): bool {
$expire = time() + $this->gcMaxLifetime;
$sql = "INSERT INTO {$this->table} (session_id, session_data, expire_time) VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE session_data = VALUES(session_data), expire_time = VALUES(expire_time)";
$stmt = $this->pdo->prepare($sql);
return $stmt->execute([$sessionId, $data, $expire]);
}
// 5. destroy: 顯式銷毀
public function destroy($sessionId): bool {
$stmt = $this->pdo->prepare("DELETE FROM {$this->table} WHERE session_id = ?");
return $stmt->execute([$sessionId]);
}
// 6. gc: 垃圾回收
public function gc($maxLifetime): int|false {
$stmt = $this->pdo->prepare("DELETE FROM {$this->table} WHERE expire_time < UNIX_TIMESTAMP()");
$stmt->execute();
return $stmt->rowCount();
}
}
// 註冊處理器
$pdo = new PDO('mysql:host=127.0.0.1;dbname=session_db;charset=utf8mb4', 'session_user', 'pass');
$handler = new MySqlSessionHandler($pdo);
session_set_save_handler($handler, true);
// 可選:調整 GC 概率
ini_set('session.gc_probability', 1);
ini_set('session.gc_divisor', 100);
ini_set('session.gc_maxlifetime', 1440);
// 啟動會話
session_start();
$_SESSION['user_id'] = 12345;
?>
PHP 內部使用 session.serialize_handler(默認 php,也可 php_binary、igbinary)。read 返回的字符串必須與 write 存入的格式完全一致,否則反序列化失敗導致 Session 數據丟失。
write,同一 Session 併發請求可能並發寫入SELECT ... FOR UPDATE 悲觀鎖(增加延遲)ON DUPLICATE KEY UPDATE 無鎖覆蓋,最後寫入勝出,Session 語義允許version 列樂觀鎖,衝突重試gc_probability/gc_divisor)不可控,高併發下可能長期不執行session.gc_probability=0),改用 Cron 定時任務 每分鐘執行 DELETE FROM php_session WHERE expire_time < UNIX_TIMESTAMP()DROP PARTITION 秒級清理歷史數據PDO::ATTR_PERSISTENT = true 或 Swoole 協程池session_data 大時 gzcompress/gzuncompress 節省存儲/網絡session_start()、讀寫、銷毀、GC