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
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;
| 方法 | 呼叫時機 | 注意事項 |
|---|---|---|
| read | session_start() | 必須回傳字串 (空字串表示無資料),不可回傳 false 否則 PHP 視為失敗 |
| write | 腳本結束 / session_write_close() | 資料已經 serialize() 過,直接存入 |
| gc | 機率觸發 (session.gc_probability/divisor) | 高流量建議改用 cron 定時清理,關閉 PHP 內建 GC |
| create_sid | session_id() 為空時 | 回傳唯一 ID,建議用 random_bytes |
PDO::ATTR_PERSISTENT => true 減少握手開銷。ini_set('session.gc_probability', 0),改用 cron 每分鐘 DELETE WHERE expire < NOW()。gzcompress(serialize($data)) 寫入,讀取時 unserialize(gzuncompress($data)),減少網傳與儲存體。session.cookie_httponly = 1、session.cookie_secure = 1 (HTTPS)session.use_strict_mode = 1 拒絕未初始化的 IDsession_id() (登入後、權限變更後)相同介面,將 PDO 換為 Redis 客戶端,write 用 SETEX key ttl data,read 用 GET,gc 不需要 (Redis 自動過期)。效能通常優於 MySQL 10 倍以上。