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

PHP PDO 事務處理完整實戰:從原理到異常回滾最佳實踐

     編輯:PHP 綜合知識

資料庫事務保證一組操作的原子性:要麼全成功,要麼全失敗回滾。本文基於 PDO + MySQL InnoDB,詳細講解事務的 ACID 特性、PHP 代碼實現模式、異常捕獲自動回滾、巢狀事務陷阱、以及生產環境最佳實踐清單。

一、事務核心概念速覽

特性說明
Atomicity (原子性)事務內所有操作視為不可分割單元,全做或全不做
Consistency (一致性)事務前後資料庫從一個一致狀態變到另一個一致狀態
Isolation (隔離性)並發事務互不干擾,MySQL 預設 REPEATABLE READ
Durability (持久性)提交後的修改永久保存,不因系統故障丟失
前提: 僅支援事務的儲存引擎 (InnoDB、BDB) 才生效。MyISAM 不支援 事務,操作會隱性提交。

二、PDO 事務標準代碼模板

setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); // 拋出異常
    $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
    
    // 2. 關閉自動提交 (開啟事務模式)
    $pdo->setAttribute(PDO::ATTR_AUTOCOMMIT, 0);
    // 等同於:$pdo->beginTransaction(); 但更明確
    
    // 3. 開始事務
    $pdo->beginTransaction();
    
    try {
        // --- 業務 SQL 操作區 ---
        $amount = 100;
        
        // 扣款
        $stmt1 = $pdo->prepare("UPDATE accounts SET balance = balance - ? WHERE user_id = ? AND balance >= ?");
        $stmt1->execute([$amount, 1, $amount]);
        if ($stmt1->rowCount() === 0) {
            throw new Exception("用戶 1 餘額不足");
        }
        
        // 入款
        $stmt2 = $pdo->prepare("UPDATE accounts SET balance = balance + ? WHERE user_id = ?");
        $stmt2->execute([$amount, 2]);
        if ($stmt2->rowCount() === 0) {
            throw new Exception("用戶 2 不存在");
        }
        
        // 記錄流水
        $stmt3 = $pdo->prepare("INSERT INTO transactions (from_id, to_id, amount, created_at) VALUES (?, ?, ?, NOW())");
        $stmt3->execute([1, 2, $amount]);
        
        // --- 結束 ---
        
        // 4. 全部成功,提交事務
        $pdo->commit();
        echo "轉帳成功";
        
    } catch (Exception $e) {
        // 5. 任一失敗,回滾事務
        $pdo->rollBack();
        echo "轉帳失敗: " . $e->getMessage();
    }
    
    // 6. 恢復自動提交模式
    $pdo->setAttribute(PDO::ATTR_AUTOCOMMIT, 1);
    
} catch (PDOException $e) {
    die("資料庫連線失敗: " . $e->getMessage());
}

// 驗證結果
$stmt = $pdo->query("SELECT user_id, balance FROM accounts WHERE user_id IN (1,2)");
foreach ($stmt as $row) {
    echo "User {$row['user_id']}: {$row['balance']}
"; } ?>

三、關鍵 API 對照

方法說明備註
beginTransaction()啟動事務,關閉自動提交返回 bool
commit()提交事務,恢復自動提交成功返回 true
rollBack()回滾事務,恢復自動提交成功返回 true
inTransaction()檢查是否在事務中PHP 5.3.3+
setAttribute(ATTR_AUTOCOMMIT, 0/1)手動控制自動提交較底層

四、進階場景與避坑指南

1. 巢狀事務陷阱 (PDO 不支援真正巢狀)

$pdo->beginTransaction(); // 外層
// ...
$pdo->beginTransaction(); // 內層 - 實際上不會開啟新事務!
// ...
$pdo->commit(); // 內層提交 - 實際提交了外層事務!
// ...
$pdo->rollBack(); // 外層回滾 - 此時已提交,回滾失效!

解決: 使用 inTransaction() 檢查,或封裝事務管理器類實現計數器模擬巢狀 (僅邏輯層面)。

2. DDL 語句隱性提交

CREATE TABLE、ALTER TABLE、TRUNCATE 等 DDL 會強制提交當前事務。事務中禁止混用 DDL。

3. 鎖等待超時 (Lock wait timeout exceeded)

  • 預設 innodb_lock_wait_timeout = 50 秒
  • 長事務易死鎖:按固定順序訪問表/行、縮短事務持續時間、避免事務中進行網路/IO 操作

4. 唯一鍵衝突不自動回滾

try {
    $pdo->beginTransaction();
    $pdo->exec("INSERT INTO users (email) VALUES ('a@b.com')"); // 成功
    $pdo->exec("INSERT INTO users (email) VALUES ('a@b.com')"); // 唯一鍵衝突,拋出異常
    $pdo->commit();
} catch (PDOException $e) {
    $pdo->rollBack(); // 必須顯式回滾,否則第一條已隱性提交!
}

5. 隔離等級設定

// 當前事務設定隔離等級
$pdo->exec("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED");
// 可選:READ UNCOMMITTED / READ COMMITTED / REPEATABLE READ / SERIALIZABLE

五、生產環境最佳實踐清單

  1. 統一使用預處理語句:防注入、效能更好、參數綁定自動轉義
  2. 異常模式 + try-catch 包裹:確保任何錯誤都能觸發 rollBack()
  3. 事務範圍最小化:只包含必要的寫操作,查詢 (SELECT) 盡量放在事務外
  4. 避免事務中呼叫外部 API/發送郵件:導致事務過長、鎖持久、併發下降
  5. 關鍵業務加入重試機制:死鎖 (SQLSTATE HY000/1213) 時指數退避重試 3 次
  6. 監控長事務:SHOW ENGINE INNODB STATUS 或監控系統告警 > 5 秒事務
  7. 測試驗證回滾:單元測試強制拋出異常,斷言資料未變更

六、替代方案:資料庫層面的原子操作

簡單場景 (如計數器增減) 可用單條 SQL 避免事務開銷:

-- 原子增減,無需事務
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1 AND balance >= 100;
-- 檢查 rowCount() 判斷成功與否

理解事務邊界、鎖行為、異常流向,配合 PDO 異常模式,即能編寫健壯、正確、高併發安全的資料庫操作代碼。

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