注意:本文內容基於 PHP 4 (2000 年),僅供歷史參考。現代 PHP (5.4+) 內建 SessionHandlerInterface,請參考官方文件實作自定儲存器。
PHP 4 預設將 Session 存為檔案 (/tmp/sess_...),分散式部署或高併發時易遇到:檔案系統 inode 耗盡、NFS 鎖效能差、多伺服器無法共享 Session。解決方案是實作 session_set_save_handler() 註冊的 6 個回調函式,將資料導向 DBM 檔案或 MySQL 資料庫。
| 函式 | 參數 | 職責 |
|---|---|---|
sess_open | $save_path, $session_name | 初始化連線 (開啟 DBM/DB 連線) |
sess_close | 無 | 關閉連線 (DBM 需關閉,MySQL 持久連線可空) |
sess_read | $key (session_id) | 讀取資料並回傳字串 (需檢查過期) |
sess_write | $key, $val (序列化資料) | 寫入資料 (附加過期時間戳) |
sess_destroy | $key | 刪除指定 session |
sess_gc | $maxlifetime | 垃圾回收:刪除過期 session |
<?php
// session_dbm.php - PHP 4 DBM Session Handler
$SESS_DBM = '';
$SESS_LIFE = get_cfg_var('session.gc_maxlifetime');
function sess_open($save_path, $session_name) {
global $SESS_DBM;
$SESS_DBM = dbmopen("$save_path/$session_name", 'c'); // 'c' = create if not exist
return $SESS_DBM;
}
function sess_close() {
global $SESS_DBM;
dbmclose($SESS_DBM);
return true;
}
function sess_read($key) {
global $SESS_DBM, $SESS_LIFE;
if ($tmp = dbmfetch($SESS_DBM, $key)) {
// 格式: expiry|data
list($expiry, $data) = explode('|', $tmp, 2);
if ($expiry > time()) return $data;
}
return '';
}
function sess_write($key, $val) {
global $SESS_DBM, $SESS_LIFE;
$expiry = time() + $SESS_LIFE;
dbmreplace($SESS_DBM, $key, "$expiry|$val");
return true;
}
function sess_destroy($key) {
global $SESS_DBM;
dbmdelete($SESS_DBM, $key);
return true;
}
function sess_gc($maxlifetime) {
global $SESS_DBM;
$now = time();
$key = dbmfirstkey($SESS_DBM);
while ($key) {
if ($tmp = dbmfetch($SESS_DBM, $key)) {
$expiry = (int)substr($tmp, 0, strpos($tmp, '|'));
if ($now > $expiry) sess_destroy($key);
}
$key = dbmnextkey($SESS_DBM, $key);
}
}
session_set_save_handler('sess_open', 'sess_close', 'sess_read', 'sess_write', 'sess_destroy', 'sess_gc');
?>
建表 SQL
CREATE DATABASE sessions;
GRANT SELECT,INSERT,UPDATE,DELETE ON sessions.* TO 'phpsession'@'localhost' IDENTIFIED BY 'phpsession';
USE sessions;
CREATE TABLE sessions (
sesskey CHAR(32) NOT NULL,
expiry INT(11) UNSIGNED NOT NULL,
value TEXT NOT NULL,
PRIMARY KEY (sesskey)
);
Handler 實作
<?php
// session_mysql.php - PHP 4 MySQL Session Handler
$SESS_DBHOST = 'localhost';
$SESS_DBNAME = 'sessions';
$SESS_DBUSER = 'phpsession';
$SESS_DBPASS = 'phpsession';
$SESS_DBH = '';
$SESS_LIFE = get_cfg_var('session.gc_maxlifetime');
function sess_open($save_path, $session_name) {
global $SESS_DBHOST, $SESS_DBNAME, $SESS_DBUSER, $SESS_DBPASS, $SESS_DBH;
$SESS_DBH = mysql_pconnect($SESS_DBHOST, $SESS_DBUSER, $SESS_DBPASS);
if (!$SESS_DBH) die('DB Connect Failed: ' . mysql_error());
mysql_select_db($SESS_DBNAME, $SESS_DBH);
return true;
}
function sess_close() { return true; } // 持久連線不關閉
function sess_read($key) {
global $SESS_DBH, $SESS_LIFE;
$key = addslashes($key);
$qry = "SELECT value FROM sessions WHERE sesskey='$key' AND expiry > " . time();
$res = mysql_query($qry, $SESS_DBH);
if ($row = mysql_fetch_row($res)) return $row[0];
return '';
}
function sess_write($key, $val) {
global $SESS_DBH, $SESS_LIFE;
$key = addslashes($key);
$val = addslashes($val);
$expiry = time() + $SESS_LIFE;
// 嘗試 INSERT,失敗則 UPDATE (主鍵衝突)
$qry = "INSERT INTO sessions (sesskey, expiry, value) VALUES ('$key', $expiry, '$val')";
if (!mysql_query($qry, $SESS_DBH)) {
$qry = "UPDATE sessions SET expiry=$expiry, value='$val' WHERE sesskey='$key' AND expiry > " . time();
mysql_query($qry, $SESS_DBH);
}
return true;
}
function sess_destroy($key) {
global $SESS_DBH;
$key = addslashes($key);
mysql_query("DELETE FROM sessions WHERE sesskey='$key'", $SESS_DBH);
return true;
}
function sess_gc($maxlifetime) {
global $SESS_DBH;
mysql_query("DELETE FROM sessions WHERE expiry < " . time(), $SESS_DBH);
return mysql_affected_rows($SESS_DBH);
}
session_set_save_handler('sess_open', 'sess_close', 'sess_read', 'sess_write', 'sess_destroy', 'sess_gc');
?>
<?php
// test.php
$handler = $_GET['handler'] ?? 'dbm'; // dbm 或 mysql
include "session_$handler.php"; // 註冊 handler
session_start();
session_register('count'); // PHP 4 風格
$action = $_GET['action'] ?? 'increment';
switch ($action) {
case 'increment': $count = ($count ?? 0) + 1; break;
case 'destroy': session_destroy(); break;
case 'gc': sess_gc(get_cfg_var('session.gc_maxlifetime')); break;
}
?>
<h1>Session Test</h1>
<p>Handler: <b><?=$handler?></b> | Action: <b><?=$action?></b> | Count: <b><?=$count?></b></p>
<form>
<select name="handler"><option value="dbm">DBM</option><option value="mysql">MySQL</option></select>
<select name="action"><option value="increment">Increment</option><option value="destroy">Destroy</option><option value="gc">GC</option></select>
<input type="submit">
</form>
<?php
// 現代寫法:實作 SessionHandlerInterface
class MySqlSessionHandler implements SessionHandlerInterface {
private $pdo;
public function __construct(PDO $pdo) { $this->pdo = $pdo; }
public function open($savePath, $sessionName) { return true; }
public function close() { return true; }
public function read($id) {
$stmt = $this->pdo->prepare('SELECT data FROM sessions WHERE id=? AND expiry>?');
$stmt->execute([$id, time()]);
return $stmt->fetchColumn() ?: '';
}
public function write($id, $data) {
$expiry = time() + ini_get('session.gc_maxlifetime');
$stmt = $this->pdo->prepare('INSERT INTO sessions (id,expiry,data) VALUES (?,?,?) ON DUPLICATE KEY UPDATE expiry=?,data=?');
return $stmt->execute([$id, $expiry, $data, $expiry, $data]);
}
public function destroy($id) {
$stmt = $this->pdo->prepare('DELETE FROM sessions WHERE id=?');
return $stmt->execute([$id]);
}
public function gc($maxlifetime) {
$stmt = $this->pdo->prepare('DELETE FROM sessions WHERE expiry');
return $stmt->execute([time()]);
}
}
// 註冊
$handler = new MySqlSessionHandler(new PDO('mysql:dbname=sessions;host=localhost', 'user', 'pass'));
session_set_save_handler($handler, true); // true = 使用 OOP 介面
session_start();
session_set_save_handler + 6 個函式實現儲存後端替換。SessionHandlerInterface + PDO,支援型別提示、依賴注入、單元測試。