首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP Session 阻塞問題完整解析:session_write_close() 解決並發請求卡死

     編輯:PHP 相關知識

PHP 預設 Session 機制會鎖定 Session 檔案,導致同一用戶的並發請求 (AJAX 長輪詢、大檔下載、多頁籤) 依序排隊執行。本文深度解析阻塞機制、提供 session_write_close() 解決方案與最佳實踐模式。

阻塞現象重現

<?php
// long_task.php - 執行 30 秒
session_start();
sleep(30);
echo "長任務完成";
?>

<?php
// quick_check.php - 即時檢查
session_start();
echo "即時回應: " . date('H:i:s');
?>

瀏覽器同時開啟兩個分頁訪問,quick_check.php 會卡住 30 秒才回應。

根本原因

官方 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.save_handler = files,資料存於檔案系統
鎖定模式session_start() 以 獨佔寫入鎖 (LOCK_EX) 開啟 Session 檔案
釋放時機腳本結束或呼叫 session_write_close() 時才釋放鎖
並發影響同一 Session ID 的請求必須排隊等待鎖釋放
方案原理優點缺點適用場景
session_write_close()手動釋放檔案鎖零依賴、相容性佳需手動管理讀寫時機大多數專案、快速修復
Redis SessionSession 存 Redis (非檔案)無檔案鎖、分散式支援、高效能需 Redis 基建、序列化開銷高併發、多伺服器叢集
Database SessionSession 存資料庫資料持久、可查詢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 架構徹底解決。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved