PHP 預設 Session 機制會鎖定 Session 檔案,導致同一用戶的並發請求 (AJAX 長輪詢、大檔下載、多頁籤) 依序排隊執行。本文深度解析阻塞機制、提供 session_write_close() 解決方案與最佳實踐模式。
| 層面 | 說明 |
| 儲存機制 | 預設 session.save_handler = files,資料存於檔案系統 |
| 鎖定模式 | session_start() 以 獨佔寫入鎖 (LOCK_EX) 開啟 Session 檔案 |
| 釋放時機 | 腳本結束或呼叫 session_write_close() 時才釋放鎖 |
| 並發影響 | 同一 Session ID 的請求必須排隊等待鎖釋放 |
官方 Bug 回應
PHP 官方立場:"This is expected, the session file is locked to avoid corruption."
Session 檔案鎖定是為防止資料損毀的預期行為,非 Bug。
解決方案:session_write_close()
<?php
// 長時間任務腳本 (AJAX 輪詢、匯出報表、大檔下載)
session_start();
// 1. 讀取必要的 Session 資料
$user_id = $_SESSION['user_id'];
$permissions = $_SESSION['permissions'];
// 2. 立即關閉 Session,釋放鎖
session_write_close();
// 3. 執行耗時操作 (此期間其他請求可正常存取 Session)
sleep(30); // 模擬耗時任務
$large_data = generate_huge_report($user_id);
// 4. 如需寫回 Session,重新開啟 (短暫鎖定)
session_start();
$_SESSION['last_report'] = $large_data;
session_write_close(); // 再次釋放
echo "任務完成";
?>
實戰案例一:OpenCart 抓取淘寶商品
<?php
// 後台 AJAX 端點:抓取商品 (耗時 5-30 分鐘)
session_start();
// 驗證權限
if (!isset($_SESSION['user_id']) || !$_SESSION['is_admin']) {
die(json_encode(['error' => 'Unauthorized']));
}
// 取得必要資料後立即釋放 Session 鎖
$user_id = $_SESSION['user_id'];
session_write_close(); // ★ 關鍵:這裡釋放鎖
// 執行長時間抓取 (此期間用戶可正常瀏覽前台、下單、查看訂單)
$taobao_api = new TaobaoApiClient($config);
$products = $taobao_api->fetchProducts($_POST['keywords'], $_POST['pages']);
$imported = 0;
foreach ($products as $product) {
$result = importProduct($product, $user_id);
if ($result) $imported++;
// 進度回寫 (短暫鎖定)
session_start();
$_SESSION['import_progress'] = [
'current' => $imported,
'total' => count($products),
'status' => 'running'
];
session_write_close();
}
// 完成標記
session_start();
$_SESSION['import_progress']['status'] = 'completed';
session_write_close();
echo json_encode(['success' => true, 'imported' => $imported]);
?>
實戰案例二:大檔案下載與進度條
<?php
// download.php
session_start();
// 驗證下載權限
if (!checkDownloadPermission($_SESSION['user_id'], $_GET['file_id'])) {
session_write_close();
http_response_code(403);
die('Forbidden');
}
$file_path = getFilePath($_GET['file_id']);
$file_size = filesize($file_path);
// 釋放 Session 鎖,允許並發請求
session_write_close();
// 限速下載 (參考文章 41867)
$speed = 50 * 1024; // 50 KB/s
$fp = fopen($file_path, 'rb');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . basename($file_path) . '"');
header('Content-Length: ' . $file_size);
while (!feof($fp) && connection_status() === CONNECTION_NORMAL) {
echo fread($fp, 8192);
flush();
// 限速邏輯...
}
fclose($fp);
?>
進階模式:Session 讀寫分離封裝類
<?php
class SessionManager {
private static $started = false;
private static $closed = false;
private static $data = [];
/**
* 安全啟動 Session (僅讀取)
*/
public static function startReadOnly() {
if (self::$started) return;
session_start();
self::$data = $_SESSION; // 複製到記憶體
session_write_close(); // 立即釋放鎖
self::$started = true;
self::$closed = true;
}
/**
* 獲取 Session 資料 (無鎖)
*/
public static function get($key, $default = null) {
if (!self::$started) self::startReadOnly();
return self::$data[$key] ?? $default;
}
/**
* 寫入 Session (短暫鎖定)
*/
public static function set($key, $value) {
if (!self::$started) self::startReadOnly();
self::$data[$key] = $value;
session_start();
$_SESSION = self::$data; // 同步回 Session
session_write_close();
}
/**
* 批次寫入 (單次鎖定)
*/
public static function setMultiple(array $data) {
if (!self::$started) self::startReadOnly();
self::$data = array_merge(self::$data, $data);
session_start();
$_SESSION = self::$data;
session_write_close();
}
/**
* 完全關閉
*/
public static function close() {
if (self::$started && !self::$closed) {
session_write_close();
self::$closed = true;
}
}
}
// 使用
SessionManager::startReadOnly();
$uid = SessionManager::get('user_id');
// 長時間任務...
sleep(10);
// 只在需要時寫入
SessionManager::set('task_progress', 50);
替代方案比較
| 方案 | 原理 | 優點 | 缺點 | 適用場景 |
| session_write_close() | 手動釋放檔案鎖 | 零依賴、相容性佳 | 需手動管理讀寫時機 | 大多數專案、快速修復 |
| Redis Session | Session 存 Redis (非檔案) | 無檔案鎖、分散式支援、高效能 | 需 Redis 基建、序列化開銷 | 高併發、多伺服器叢集 |
| Database Session | Session 存資料庫 | 資料持久、可查詢 | DB 連線開銷、鎖退化為行鎖 | 需審計追蹤的系統 |
| JWT 無狀態 | Client 端攜帶 Token | 完全無鎖、水平擴展極佳 | Token 撤銷複雜、載荷大小限制 | 微服務、API 為主架構 |
Redis Session 快速配置
; php.ini
session.save_handler = redis
session.save_path = "tcp://127.0.0.1:6379?auth=your_password&prefix=session_"
; 或程式碼設定
ini_set('session.save_handler', 'redis');
ini_set('session.save_path', 'tcp://127.0.0.1:6379');
session_start();
常見誤區
- ❌ 以為 session_start() 只讀不寫不會鎖 → 預設開啟即為獨佔鎖
- ❌ 在輸出 HTML 後才呼叫 session_write_close() → 應在讀取完 Session 立即呼叫
- ❌ 以為 session_commit() 不同 → 它是
session_write_close() 的別名
- ❌ 長任務中重複 session_start()/write_close() 過於頻繁 → 批次寫入、減少切換
Session 阻塞是 PHP 經典效能陷阱。核心原則:儘早釋放鎖,儘晚獲取鎖,縮小鎖定範圍。現代高併發系統建議遷移至 Redis Session 或 JWT 架構徹底解決。
- 上一篇文章: PHP 大陣列 in_array 效能優化:鍵值翻轉與字串搜尋兩大高速策略
- 下一篇文章: PHP 敏感資料遮罩完整指南:用戶名、身份證、IP、手機號、銀行卡星號隱藏實作
Copyright ©
程式碼之家 All Rights Reserved