隨著網站規模擴大,單一伺服器已無法滿足流量需求。典型的架構會部署多台 Web 伺服器,各自運行不同功能模組(如論壇、商城、部落格),透過不同二級域名對外服務。雖然用戶資料可集中存放於資料庫實現共享,但「一次登入,處處通行」的 Session 共享問題卻成為開發難點:用戶在伺服器 A 登入後,跳轉至伺服器 B 卻需重新登入。
當客戶端首次訪問並呼叫 session_start() 時,PHP 會生成唯一的 Session ID(可透過 session_id() 取得)。此 ID 須保留在客戶端,以便後續請求識別身份,主要有兩種方式:
PHPSESSIDPHPSESSID(主流做法)Session 資料預設以檔案形式儲存於伺服器(session.save_handler = files),路徑由 session.save_path 指定,檔案命名格式為 sess_{SessionID},內容為序列化後的資料。高流量下可啟用分級目錄(session.save_path="N;/save_path")提升 I/O 效能。
預設情況下,各伺服器獨立生成 Session ID 且資料各自儲存在本地檔案系統,導致無法互通。解決方案需達成雙重目標:
預設 Cookie 僅屬當前域名。將 session.cookie_domain 設為共同根域名(如 .example.com),即可讓所有子域名伺服器共享同一 PHPSESSID Cookie。
CREATE TABLE `sess` (
`sesskey` varchar(32) NOT NULL DEFAULT '',
`expiry` bigint(20) NOT NULL DEFAULT '0',
`data` longtext NOT NULL,
PRIMARY KEY (`sesskey`),
KEY `expiry` (`expiry`)
) ENGINE=MyISAM;
sesskey 對應 Session ID,expiry 記錄過期時間戳,data 存放序列化資料。
透過 session_set_save_handler() 覆寫六大核心操作(open、close、read、write、destroy、gc),並將 session.save_handler 設為 user。
<?php
define('MY_SESS_TIME', 3600); // Session 生存秒數
class My_Sess {
static function init() {
$domain = '.example.com'; // 修改為您的根域名
ini_set('session.use_trans_sid', 0); // 禁用 URL 重寫
ini_set('session.gc_maxlifetime', MY_SESS_TIME);
ini_set('session.use_cookies', 1); // 啟用 Cookie
ini_set('session.cookie_path', '/'); // 路徑根目錄
ini_set('session.cookie_domain', $domain); // 關鍵:共享域
session_module_name('user'); // 切換至用戶自定義模式
session_set_save_handler(
array('My_Sess', 'open'),
array('My_Sess', 'close'),
array('My_Sess', 'read'),
array('My_Sess', 'write'),
array('My_Sess', 'destroy'),
array('My_Sess', 'gc')
);
}
static function open($save_path, $session_name) { return true; }
static function close() {
global $MY_SESS_CONN;
if ($MY_SESS_CONN) $MY_SESS_CONN->Close();
return true;
}
static function read($sesskey) {
global $MY_SESS_CONN;
$sql = 'SELECT data FROM sess WHERE sesskey=' . $MY_SESS_CONN->qstr($sesskey) . ' AND expiry>=' . time();
$rs = &$MY_SESS_CONN->Execute($sql);
if ($rs && !$rs->EOF) {
$v = $rs->fields[0];
$rs->Close();
return $v;
}
return '';
}
static function write($sesskey, $data) {
global $MY_SESS_CONN;
$qkey = $MY_SESS_CONN->qstr($sesskey);
$expiry = time() + MY_SESS_TIME;
$arr = array('sesskey' => $qkey, 'expiry' => $expiry, 'data' => $data);
$MY_SESS_CONN->Replace('sess', $arr, 'sesskey', $autoQuote = true);
return true;
}
static function destroy($sesskey) {
global $MY_SESS_CONN;
$sql = 'DELETE FROM sess WHERE sesskey=' . $MY_SESS_CONN->qstr($sesskey);
$MY_SESS_CONN->Execute($sql);
return true;
}
static function gc($maxlifetime = null) {
global $MY_SESS_CONN;
$MY_SESS_CONN->Execute('DELETE FROM sess WHERE expiry<' . time());
$MY_SESS_CONN->Execute('OPTIMIZE TABLE sess'); // 碎片整理
return true;
}
}
// 資料庫連線設定
require_once('adodb/adodb.inc.php');
$db_type = 'mysql';
$db_host = '192.168.1.100';
$db_user = 'sess_user';
$db_pass = 'sess_pass';
$db_name = 'sess_db';
$GLOBALS['MY_SESS_CONN'] =& ADONewConnection($db_type);
$GLOBALS['MY_SESS_CONN']->Connect($db_host, $db_user, $db_pass, $db_name);
// 必須在 session_start() 之前呼叫!
My_Sess::init();
?>
高併發場景下,資料庫頻繁讀寫成為瓶頸。建議考慮:
選擇方案時需權衡一致性、可用性與延遲需求,資料庫方案適合中小型專案,大型系統建議採用分散式快取叢集。