預設 PHP 將 Session 儲存於伺服器檔案系統,在多伺服器叢集環境下會導致 Session 不一致。本文提供完整的 MySQL 儲存 Session 實作方案,使用 PDO 預處理語句確保安全效能,解決分散式部署的 Session 共享難題。
CREATE TABLE `session` (
`skey` CHAR(32) CHARACTER SET ascii NOT NULL,
`data` TEXT COLLATE utf8mb4_bin,
`expire` INT(11) NOT NULL,
PRIMARY KEY (`skey`),
KEY `index_session_expire` (`expire`) USING BTREE
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
設計重點:
skey 使用 ASCII 字元集節省空間data 使用 utf8mb4 支援完整 Unicodeexpire 整數時間戳,配合索引快速清理過期資料<?php
// 資料庫連線常量
define('SESSION_DSN', 'mysql:host=localhost;dbname=db;charset=utf8mb4');
define('SESSION_USR', 'usr');
define('SESSION_PWD', 'pwd');
define('SESSION_MAXLIFETIME', get_cfg_var('session.gc_maxlifetime'));
// 建立 PDO 連線(持久連線提升效能)
function getConnection() {
try {
return new PDO(SESSION_DSN, SESSION_USR, SESSION_PWD, array(
PDO::ATTR_PERSISTENT => true,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES=> false,
));
} catch (Exception $e) {
return null;
}
}
// 1. open - 開啟 Session
function sessionMysqlOpen($savePath, $sessionName) { return true; }
// 2. close - 關閉 Session
function sessionMysqlClose() { return true; }
// 3. read - 讀取 Session 資料
function sessionMysqlRead($sessionId) {
try {
$dbh = getConnection();
$time = time();
// 先檢查是否存在且未過期
$stmt = $dbh->prepare('SELECT COUNT(*) AS `count` FROM `session` WHERE `skey` = ? AND `expire` > ?');
$stmt->execute(array($sessionId, $time));
if ($stmt->fetchColumn() == 0) return '';
// 讀取資料
$stmt = $dbh->prepare('SELECT `data` FROM `session` WHERE `skey` = ? AND `expire` > ?');
$stmt->execute(array($sessionId, $time));
return $stmt->fetchColumn() ?: '';
} catch (Exception $e) { return ''; }
}
// 4. write - 寫入/更新 Session 資料
function sessionMysqlWrite($sessionId, $data) {
try {
$dbh = getConnection();
$expire = time() + SESSION_MAXLIFETIME;
// INSERT ... ON DUPLICATE KEY UPDATE 原子操作
$stmt = $dbh->prepare('INSERT INTO `session` (`skey`,`data`,`expire`) VALUES (?,?,?) ON DUPLICATE KEY UPDATE `data`=?, `expire`=?');
$stmt->execute(array($sessionId, $data, $expire, $data, $expire));
} catch (Exception $e) { error_log($e->getMessage()); }
}
// 5. destroy - 刪除 Session
function sessionMysqlDestroy($sessionId) {
try {
$dbh = getConnection();
$stmt = $dbh->prepare('DELETE FROM `session` WHERE `skey` = ?');
$stmt->execute(array($sessionId));
return true;
} catch (Exception $e) { return false; }
}
// 6. gc - 清理過期 Session
function sessionMysqlGc($lifetime) {
try {
$dbh = getConnection();
$stmt = $dbh->prepare('DELETE FROM `session` WHERE `expire` < ?');
$stmt->execute(array(time()));
return true;
} catch (Exception $e) { return false; }
}
// 7. 自定義 Session ID 生成(避免多伺服器衝突)
function sessionMysqlId() {
if (filter_input(INPUT_GET, session_name()) === '' && filter_input(INPUT_COOKIE, session_name()) === '') {
try {
$dbh = getConnection();
$uuid = $dbh->query('SELECT UUID() AS uuid')->fetchColumn();
session_id(str_replace('-', '', $uuid));
return true;
} catch (Exception $e) { return false; }
}
}
// 統一啟動函數
function startSession() {
session_set_save_handler(
'sessionMysqlOpen',
'sessionMysqlClose',
'sessionMysqlRead',
'sessionMysqlWrite',
'sessionMysqlDestroy',
'sessionMysqlGc'
);
register_shutdown_function('session_write_close');
sessionMysqlId();
session_start();
}
?>
<?php require_once 'session_mysql.php'; startSession(); // 替代原本的 session_start() // 正常使用 $_SESSION $_SESSION['user_id'] = 123; $_SESSION['username'] = 'admin'; ?>
| 技術點 | 說明 |
|---|---|
| ON DUPLICATE KEY UPDATE | MySQL 特有語法,單一查詢完成「不存在則插入、存在則更新」,避免競態條件 |
| 預處理語句 | 防止 SQL 注入,PDO::ATTR_EMULATE_PREPARES=false 使用真實預處理 |
| 持久連線 | PDO::ATTR_PERSISTENT=true 複用連線,減少建立連線開銷 |
| expire > time() | 讀取時同時檢查過期,避免回傳失效資料,GC 只需清理資料庫 |
| UUID() 生成 ID | MySQL 原生 UUID 確保分散式環境下 ID 唯一性,避免衝突 |
| register_shutdown_function | 確保腳本結束時正確寫入 Session,異常終止也能保存 |
ENGINE=MEMORY,重啟後資料遺失但效能極致。skey 首字元分 16/256 表,降低單表鎖定爭用。session.gc_probability=0),改用 Crontab 定時清理。提醒:單機或低流量應用無需此方案,PHP 原生檔案 Session 效能更好且零配置。架構升級應基於實際壓力測試數據決策,避免過度設計。