預設 Session 儲存在檔案系統,分散式部署、高併發場景下存在效能與一致性問題。使用 session_set_save_handler() 實現資料庫儲存,支援叢集共享、在線用戶統計、精準過期控制。
<?php
class Session {
private $db;
private $lasttime = 3600; // 過期時間:1小時
function __construct(&$db) {
$this->db = &$db;
session_module_name('user'); // 必須:宣告使用用戶自定義處理器
session_set_save_handler(
array(&$this, 'open'), // session_start() 時執行
array(&$this, 'close'), // 腳本結束/ session_write_close() 時執行
array(&$this, 'read'), // session_start() 時讀取資料
array(&$this, 'write'), // 腳本結束/ session_write_close() 時寫入
array(&$this, 'destroy'), // session_destroy() 時執行
array(&$this, 'gc') // GC 觸發時執行 (機率由 gc_probability/divisor 決定)
);
session_start(); // 必須在註冊處理器後調用
}
// 解析 PHP 序列化 Session 資料
function unserializes($data_value) {
$vars = preg_split(
'/([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)\|/',
$data_value, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE
);
for ($i = 0; isset($vars[$i]); $i++) {
$result[$vars[$i++]] = unserialize($vars[$i]);
}
return $result;
}
function open($path, $name) { return true; }
function close() {
$this->gc($this->lasttime);
return true;
}
function read($sessionKey) {
$sql = "SELECT uid FROM sessions WHERE session_id = '$sessionKey' LIMIT 1";
$query = $this->db->query($sql);
if ($row = $this->db->fetch_array($query)) {
return $row['uid']; // 返回序列化資料 (這裡簡化只返回 uid)
}
return "";
}
function write($sessionKey, $data) {
// 重新連接資料庫 (長連接可能斷開)
require_once('db_mysql_class.php');
$db1 = new DbCom();
$db1->connect(DB_SERVER, DB_SERVER_USERNAME, DB_SERVER_PASSWORD, DB_DATABASE);
$db1->query("SET NAMES utf8");
$this->db = $db1;
$sessionData = $this->unserializes($data);
$sql0 = "SELECT uid FROM sessions WHERE session_id = '$sessionKey' LIMIT 1";
$query0 = $this->db->query($sql0);
if ($this->db->num_rows($query0) <= 0) {
// 新 Session
if (isset($sessionData['webid']) && !empty($sessionData['webid'])) {
$this->db->query("INSERT INTO `sessions` SET
`session_id` = '$sessionKey',
`uid` = '{$sessionData['webid']}',
`last_visit` = '" . time() . "'");
}
} else {
// 現有 Session:可選擇更新 last_visit
// $this->db->query("UPDATE `sessions` SET `last_visit` = '" . time() . "' WHERE `session_id` = '$sessionKey'");
}
return true;
}
function destroy($sessionKey) {
$this->db->query("DELETE FROM `sessions` WHERE `session_id` = '$sessionKey'");
return true;
}
function gc($lifetime) {
$this->db->query("DELETE FROM `sessions` WHERE UNIX_TIMESTAMP(NOW()) - `last_visit` > '$this->lasttime'");
return true;
}
}
CREATE TABLE `sessions` (
`session_id` CHAR(32) NOT NULL PRIMARY KEY,
`uid` INT(10) UNSIGNED NOT NULL DEFAULT 0,
`last_visit` INT(10) UNSIGNED NOT NULL DEFAULT 0,
INDEX `idx_last_visit` (`last_visit`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
| 配置項 | 推薦值 | 說明 |
|---|---|---|
| session.save_handler | user | 啟用自定義處理器 |
| session.save_path | /tmp | 檔案模式路徑;user 模式下被忽略 |
| session.gc_maxlifetime | 1440 | GC 判定過期秒數 |
| session.gc_probability | 1 | |
| session.gc_divisor | 1000 | 高流量建議 1000-5000 |
| session.use_cookies | On | |
| session.use_only_cookies | On | 防 Session Fixation |
| session.cookie_httponly | On | 防 XSS 竊取 ID |
| session.hash_function | 1 (SHA-1) | 更安全 |
| session.hash_bits_per_character | 5 | 0-9,a-v (較短 ID) |
session_start()
↓
open() → read() → [GC 執行] → 腳本執行
↓
腳本結束 / session_write_close()
↓
write() → close()
↓
session_destroy() → destroy()
write() 中重新連接資料庫:長腳本執行期間連接可能超時斷開read() 必須返回序列化字串,write() 接收序列化字串PDO + Prepared Statements 防 SQL 注入Redis / Memcached 做 Session 儲存此實作展示了 Session 處理器的完整介面契約,可作為資料庫/快取儲存 Session 的參考基礎。