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

PHP 交易轉帳實作:Mysqli 與 PDO 事務處理完整範例與避坑指南

     編輯:PHP 綜合知識

金融轉帳、扣款入帳等涉及多筆資料庫寫入的操作,必須使用資料庫事務 確保原子性:要么全部成功,要么全部回滾。本文提供 Mysqli(程序式/物件導向)與 PDO 兩套完整實作範例,涵蓋關閉自動提交、錯誤處理、回滾機制、例外捕獲,並說明常見陷阱與最佳實務。

核心概念:ACID 與事務控制流程

sequenceDiagram
    participant App
    participant DB
    App->>DB: BEGIN / autocommit=0
    App->>DB: UPDATE account SET balance=balance-1000 WHERE id=3
    alt 扣款失敗
        App->>DB: ROLLBACK
        App-->>User: 轉帳失敗
    else 扣款成功
        App->>DB: UPDATE account SET balance=balance+1000 WHERE id=2
        alt 入帳失敗
            App->>DB: ROLLBACK
            App-->>User: 轉帳失敗
        else 入帳成功
            App->>DB: COMMIT
            App-->>User: 轉帳成功
        end
    end

方案一:Mysqli 物件導向風格(推薦)

<?php
$mysqli = new mysqli('localhost', 'root', 'password', 'bank_db');
$mysqli->set_charset('utf8mb4');

if ($mysqli->connect_errno) {
    die('DB 連線失敗: ' . $mysqli->connect_error);
}

try {
    $mysqli->begin_transaction(MYSQLI_TRANS_START_READ_WRITE); // 關閉自動提交
    $flag = true;

    // 扣款
    $res = $mysqli->query("UPDATE account SET balance = balance - 1000 WHERE id = 3");
    if (!$res || $mysqli->affected_rows === 0) {
        throw new Exception('扣款失敗:帳戶不存在或餘額不足');
    }

    // 入帳
    $res = $mysqli->query("UPDATE account SET balance = balance + 1000 WHERE id = 2");
    if (!$res || $mysqli->affected_rows === 0) {
        throw new Exception('入帳失敗:目標帳戶不存在');
    }

    $mysqli->commit();
    echo '轉帳成功';

} catch (Throwable $e) {
    $mysqli->rollback();
    error_log('轉帳異常: ' . $e->getMessage());
    echo '轉帳失敗: ' . $e->getMessage();
} finally {
    $mysqli->close();
}

方案二:PDO + 例外模式(更現代、可攜性佳)

<?php
$dsn = 'mysql:host=localhost;dbname=bank_db;charset=utf8mb4';
$pdo = new PDO($dsn, 'root', 'password', [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION, // 拋出例外
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES   => false, // 使用真實預處理
]);

try {
    $pdo->beginTransaction();

    // 使用預處理語句防 SQL 注入
    $stmt = $pdo->prepare('UPDATE account SET balance = balance - :amt WHERE id = :id');
    $stmt->execute(['amt' => 1000, 'id' => 3]);
    if ($stmt->rowCount() === 0) throw new Exception('扣款失敗');

    $stmt = $pdo->prepare('UPDATE account SET balance = balance + :amt WHERE id = :id');
    $stmt->execute(['amt' => 1000, 'id' => 2]);
    if ($stmt->rowCount() === 0) throw new Exception('入帳失敗');

    $pdo->commit();
    echo '轉帳成功';

} catch (Throwable $e) {
    if ($pdo->inTransaction()) $pdo->rollBack();
    error_log('轉帳異常: ' . $e->getMessage());
    echo '轉帳失敗: ' . $e->getMessage();
}

方案三:原版程式碼的改良版(保留邏輯,修正語法錯誤)

<?php
// 原版有語法錯誤:if(!result ...) 應為 if(!$result ...)
// 以下為修正後可執行版本
$mysqli = new mysqli("localhost", "root", "064319", "php");
$mysqli->set_charset("utf8");

if ($mysqli->connect_errno) die('連線失敗: ' . $mysqli->connect_error);

$mysqli->autocommit(false);
$flag = true;

$res = $mysqli->query("UPDATE account SET balance=balance-1000 WHERE id=3");
if (!$res || $mysqli->affected_rows === 0) $flag = false;

$res = $mysqli->query("UPDATE account SET balance=balance+1000 WHERE id=2");
if (!$res || $mysqli->affected_rows === 0) $flag = false;

if ($flag) {
    $mysqli->commit();
    echo '轉帳成功';
} else {
    $mysqli->rollback();
    echo '轉帳失敗';
}
$mysqli->autocommit(true);
$mysqli->close();

關鍵避坑指南

陷阱後果解決方案
未關閉 autocommit每句自動 commit,無法回滾`$mysqli->autocommit(false)` 或 `$pdo->beginTransaction()`
只檢查 `$res` 不檢查 `affected_rows`WHERE 不匹配仍回傳 true,資料實際未更新必須雙重檢查:`!$res || $affected_rows === 0`
使用 `mysql_*` 已棄用函式PHP 7+ 移除,安全性差改用 mysqli / PDO
catch 只接 `Exception` 不接 `Throwable`PHP 7+ Error 類錯誤無法捕獲`catch (Throwable $e)`
事務中執行 DDL(CREATE/ALTER/DROP)MySQL 隱性 commit,破壞事務事務中僅執行 DML(INSERT/UPDATE/DELETE)
長事務鎖定資源阻塞其他連線,導致死鎖或超時事務內盡量少查詢、快進快出、避免網路呼叫

進階:樂觀鎖防止超賣/餘額不足

高併發下,單純 `balance - 1000` 可能導致負餘額。加上版本號或條件更新:

UPDATE account
SET balance = balance - 1000, version = version + 1
WHERE id = 3 AND balance >= 1000 AND version = :current_version

若 `affected_rows === 0` 則代表餘額不足或版本衝突,觸發重試或回滾。

掌握事務正確用法,是開發可靠金融、電商、庫存系統的基石。建議專案統一封裝 `TransactionManager` 或使用框架內建的 `DB::transaction()` 以減少樣板碼與人為錯誤。

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