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

PHP PDO MySQL 交易處理完整實戰範例

     編輯:PHP 相關知識

為什麼需要資料庫交易?

交易確保一組相關的 SQL 操作要麼全部成功,要麼全部失敗回滾,維護資料一致性。經典場景:轉帳扣款與入帳必須同時成功,否則資料會不一致。

MySQL 交易前提條件

  • 資料表引擎必須為 InnoDB(MyISAM 不支援交易)。
  • PHP 使用 PDO 擴充套件(建議開啟 PDO::ATTR_ERRMODE_EXCEPTION)。

核心流程四步驟

  1. 關閉自動提交:$pdo->setAttribute(PDO::ATTR_AUTOCOMMIT, 0);
  2. 開始交易:$pdo->beginTransaction();
  3. 執行 SQL 操作:若任一步驟失敗,拋出例外觸發回滾。
  4. 提交或回滾:成功 $pdo->commit();,失敗 $pdo->rollback();
  5. 恢復自動提交:$pdo->setAttribute(PDO::ATTR_AUTOCOMMIT, 1);

完整程式碼範例:模擬轉帳情境

<?php
// 資料庫連線設定
$dsn = "mysql:host=localhost;dbname=bank;charset=utf8mb4";
$user = "root";
$pass = "password";

try {
    $pdo = new PDO($dsn, $user, $pass, [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        PDO::ATTR_EMULATE_PREPARES => false,
    ]);
} catch (PDOException $e) {
    die("資料庫連線失敗:" . $e->getMessage());
}

/**
 * 轉帳函式:從 $from_id 扣款 $amount 並存入 $to_id
 * 回傳 true 成功,false 失敗
 */
function transfer($pdo, $from_id, $to_id, $amount) {
    try {
        // 1. 關閉自動提交
        $pdo->setAttribute(PDO::ATTR_AUTOCOMMIT, 0);
        // 2. 開始交易
        $pdo->beginTransaction();

        // 3. 扣款(檢查餘額)
        $stmt = $pdo->prepare("SELECT balance FROM accounts WHERE id = ? FOR UPDATE");
        $stmt->execute([$from_id]);
        $from = $stmt->fetch();
        if (!$from || $from['balance'] < $amount) {
            throw new Exception("餘額不足或帳戶不存在");
        }
        $pdo->prepare("UPDATE accounts SET balance = balance - ? WHERE id = ?")
            ->execute([$amount, $from_id]);

        // 4. 入帳
        $pdo->prepare("UPDATE accounts SET balance = balance + ? WHERE id = ?")
            ->execute([$amount, $to_id]);

        // 5. 記錄交易流水
        $pdo->prepare("INSERT INTO transactions (from_id, to_id, amount, created_at) VALUES (?, ?, ?, NOW())")
            ->execute([$from_id, $to_id, $amount]);

        // 6. 全部成功,提交交易
        $pdo->commit();
        echo "轉帳成功!\n";
        return true;

    } catch (Exception $e) {
        // 7. 發生任何錯誤,回滾
        if ($pdo->inTransaction()) {
            $pdo->rollback();
        }
        echo "轉帳失敗:" . $e->getMessage() . "\n";
        return false;
    } finally {
        // 8. 還原自動提交模式
        $pdo->setAttribute(PDO::ATTR_AUTOCOMMIT, 1);
    }
}

// 測試
transfer($pdo, 1, 2, 500); // 張三轉 500 給李四
?>

關鍵細節解析

1. SELECT ... FOR UPDATE

在交易中查詢餘額時加上 FOR UPDATE,會對該列加排他鎖,防止並發交易同時讀取舊餘額導致超扣(競態條件)。

2. 例外處理與回滾時機

  • 任何 PDOException、自訂 Exception、或 execute() 回傳 false 都應觸發回滾。
  • 使用 finally 區塊確保自動提交模式一定會被還原,避免影響後續非交易查詢。

3. 巢狀交易注意事項

PDO 不支援真正的巢狀交易。若在交易中呼叫另一個也開啟交易的函式,內層 commit() 實際上不會寫入,直到最外層 commit()。建議交易邏輯集中在單一函式,或使用 Savepoint(SAVEPOINT name / ROLLBACK TO SAVEPOINT name)實現部分回滾。

常見錯誤與除錯

錯誤現象 原因 解決
SQLSTATE[HY000]: General error: 2014 Cannot execute queries while other unbuffered queries are active 同一連線有未讀完的結果集 確保前一個查詢的結果集已全部 fetch 或 closeCursor()
Deadlock found when trying to get lock; try restarting transaction 多執行緒並發更新同筆資料順序不一致 統一更新順序(如按 id 排序)、縮短交易時間、重試機制
Lock wait timeout exceeded 等待鎖釋放超過 innodb_lock_wait_timeout(預設 50 秒) 檢查長時間執行的交易、優化索引、調大 timeout

進階:使用 Savepoint 實現部分回滾

$pdo->beginTransaction();
try {
    $pdo->exec("INSERT INTO logs (msg) VALUES ('step 1')");
    $pdo->exec("SAVEPOINT step1");

    $pdo->exec("INSERT INTO logs (msg) VALUES ('step 2 - might fail')");
    // 若這步失敗,只回滾到 step1
    $pdo->exec("SAVEPOINT step2");

    $pdo->exec("INSERT INTO logs (msg) VALUES ('step 3')");
    $pdo->commit();
} catch (Exception $e) {
    $pdo->exec("ROLLBACK TO SAVEPOINT step1"); // 保留 step1
    $pdo->commit(); // 提交 step1
}
?>
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved