PHP 保存点实现事务部分回滚

wen PHP项目 3

本文目录导读:

PHP 保存点实现事务部分回滚

  1. MySQLi 实现保存点
  2. PDO 实现保存点
  3. 完整的业务场景示例
  4. 保存点操作的注意事项
  5. 最佳实践建议

在 PHP 中,保存点(Savepoint)允许你在事务中设置标记点,以便在出错时可以部分回滚到指定位置,而不是回滚整个事务,以下是完整的实现方法:

MySQLi 实现保存点

<?php
$mysqli = new mysqli("localhost", "user", "password", "database");
if ($mysqli->connect_error) {
    die("连接失败: " . $mysqli->connect_error);
}
$mysqli->autocommit(FALSE);
try {
    // 开启事务
    $mysqli->begin_transaction();
    // 第一步操作
    $mysqli->query("INSERT INTO users (name, email) VALUES ('张三', 'zhangsan@example.com')");
    $user_id = $mysqli->insert_id;
    // 设置保存点
    $mysqli->query("SAVEPOINT sp_user_created");
    // 第二步操作 - 这里可能会出错
    $mysqli->query("INSERT INTO orders (user_id, amount) VALUES ($user_id, 100)");
    $order_id = $mysqli->insert_id;
    // 设置另一个保存点
    $mysqli->query("SAVEPOINT sp_order_created");
    // 第三步操作 - 模拟一个错误
    $result = $mysqli->query("UPDATE accounts SET balance = balance - 100 WHERE user_id = $user_id");
    if (!$result) {
        // 回滚到 sp_order_created 保存点
        $mysqli->query("ROLLBACK TO SAVEPOINT sp_order_created");
        echo "回滚到订单创建后的状态<br>";
    }
    // 检查数据状态
    echo "用户ID: " . $user_id . "<br>";
    echo "订单ID: " . $order_id . "<br>";
    // 提交事务
    $mysqli->commit();
    echo "事务成功提交";
} catch (Exception $e) {
    // 完全回滚
    $mysqli->rollback();
    echo "事务回滚: " . $e->getMessage();
}
$mysqli->close();
?>

PDO 实现保存点

<?php
try {
    $pdo = new PDO(
        "mysql:host=localhost;dbname=database",
        "user",
        "password",
        [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
    );
    // 开启事务
    $pdo->beginTransaction();
    // 第一步操作
    $stmt = $pdo->prepare("INSERT INTO users (name, email) VALUES (?, ?)");
    $stmt->execute(['李四', 'lisi@example.com']);
    $user_id = $pdo->lastInsertId();
    // 创建保存点
    $pdo->exec("SAVEPOINT sp_user_created");
    try {
        // 第二步操作
        $stmt = $pdo->prepare("INSERT INTO orders (user_id, amount) VALUES (?, ?)");
        $stmt->execute([$user_id, 200]);
        $order_id = $pdo->lastInsertId();
        // 创建第二个保存点
        $pdo->exec("SAVEPOINT sp_order_created");
        // 第三步操作 - 可能会失败
        $stmt = $pdo->prepare("UPDATE inventory SET stock = stock - 1 WHERE product_id = ?");
        $success = $stmt->execute([1]);
        if (!$success) {
            // 回滚到订单保存点
            $pdo->exec("ROLLBACK TO SAVEPOINT sp_order_created");
            echo "回滚到订单创建点<br>";
        }
        // 提交事务
        $pdo->commit();
        echo "事务提交成功";
    } catch (Exception $e) {
        // 回滚到用户创建保存点
        $pdo->exec("ROLLBACK TO SAVEPOINT sp_user_created");
        echo "回滚到用户创建点: " . $e->getMessage() . "<br>";
        // 可以继续执行其他操作或提交
        $pdo->commit();
    }
} catch (Exception $e) {
    // 完全回滚
    if (isset($pdo) && $pdo->inTransaction()) {
        $pdo->rollBack();
    }
    echo "错误: " . $e->getMessage();
}
?>

完整的业务场景示例

<?php
class TransactionManager {
    private $pdo;
    public function __construct(PDO $pdo) {
        $this->pdo = $pdo;
    }
    /**
     * 复杂订单处理事务
     */
    public function processOrder($userId, $products) {
        try {
            // 开始事务
            $this->pdo->beginTransaction();
            // 1. 创建订单
            $orderId = $this->createOrder($userId);
            // 设置保存点
            $this->pdo->exec("SAVEPOINT sp_order");
            // 2. 添加订单商品
            $this->addOrderItems($orderId, $products);
            // 设置保存点
            $this->pdo->exec("SAVEPOINT sp_items");
            // 3. 更新库存(可能失败)
            try {
                $this->updateInventory($products);
            } catch (Exception $e) {
                // 库存更新失败,回滚到添加商品前
                $this->pdo->exec("ROLLBACK TO SAVEPOINT sp_items");
                echo "库存更新失败,已回滚商品添加<br>";
                // 继续处理,不阻塞整个事务
                $this->pdo->exec("SAVEPOINT sp_after_inventory_failure");
            }
            // 4. 处理支付(可能失败)
            try {
                $this->processPayment($orderId);
            } catch (Exception $e) {
                // 支付失败,回滚到订单创建后
                $this->pdo->exec("ROLLBACK TO SAVEPOINT sp_order");
                // 添加订单失败记录
                $this->logFailedOrder($userId, $e->getMessage());
                $this->pdo->commit();
                return ['success' => false, 'message' => '支付失败', 'orderId' => $orderId];
            }
            // 提交事务
            $this->pdo->commit();
            return ['success' => true, 'message' => '订单处理成功', 'orderId' => $orderId];
        } catch (Exception $e) {
            // 完全回滚
            if ($this->pdo->inTransaction()) {
                $this->pdo->rollBack();
            }
            return ['success' => false, 'message' => $e->getMessage()];
        }
    }
    private function createOrder($userId) {
        $stmt = $this->pdo->prepare("INSERT INTO orders (user_id, status) VALUES (?, 'pending')");
        $stmt->execute([$userId]);
        return $this->pdo->lastInsertId();
    }
    private function addOrderItems($orderId, $products) {
        $stmt = $this->pdo->prepare(
            "INSERT INTO order_items (order_id, product_id, quantity) VALUES (?, ?, ?)"
        );
        foreach ($products as $product) {
            $stmt->execute([$orderId, $product['product_id'], $product['quantity']]);
        }
    }
    private function updateInventory($products) {
        $stmt = $this->pdo->prepare(
            "UPDATE products SET stock = stock - ? WHERE product_id = ? AND stock >= ?"
        );
        foreach ($products as $product) {
            $stmt->execute([
                $product['quantity'], 
                $product['product_id'], 
                $product['quantity']
            ]);
            if ($stmt->rowCount() === 0) {
                throw new Exception("库存不足: 产品ID " . $product['product_id']);
            }
        }
    }
    private function processPayment($orderId) {
        // 模拟支付处理
        if (rand(0, 10) > 7) { // 30% 失败率
            throw new Exception("支付处理失败");
        }
        $stmt = $this->pdo->prepare("UPDATE orders SET status = 'paid' WHERE id = ?");
        $stmt->execute([$orderId]);
    }
    private function logFailedOrder($userId, $error) {
        $stmt = $this->pdo->prepare(
            "INSERT INTO order_failures (user_id, error_message) VALUES (?, ?)"
        );
        $stmt->execute([$userId, $error]);
    }
}
// 使用示例
$pdo = new PDO("mysql:host=localhost;dbname=database", "user", "password");
$manager = new TransactionManager($pdo);
$result = $manager->processOrder(1, [
    ['product_id' => 1, 'quantity' => 2],
    ['product_id' => 2, 'quantity' => 1]
]);
print_r($result);
?>

保存点操作的注意事项

<?php
// 检查是否支持保存点
function checkSavepointSupport($pdo) {
    $driver = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
    switch ($driver) {
        case 'mysql':
            // MySQL 5.0+ 支持
            return true;
        case 'pgsql':
            // PostgreSQL 8.0+ 支持
            return true;
        case 'sqlite':
            // SQLite 3.0+ 支持
            return true;
        case 'sqlsrv':
            // SQL Server 2005+ 支持
            return true;
        default:
            return false;
    }
}
// 封装保存点操作类
class SavepointHelper {
    private $pdo;
    private $savepoints = [];
    public function __construct(PDO $pdo) {
        $this->pdo = $pdo;
    }
    public function create($name) {
        $this->pdo->exec("SAVEPOINT $name");
        array_push($this->savepoints, $name);
        return true;
    }
    public function rollbackTo($name) {
        if (in_array($name, $this->savepoints)) {
            $this->pdo->exec("ROLLBACK TO SAVEPOINT $name");
            // 移除该保存点之后的所有保存点
            while (end($this->savepoints) !== $name) {
                array_pop($this->savepoints);
            }
            return true;
        }
        return false;
    }
    public function release($name) {
        $this->pdo->exec("RELEASE SAVEPOINT $name");
        $key = array_search($name, $this->savepoints);
        if ($key !== false) {
            unset($this->savepoints[$key]);
        }
        return true;
    }
    public function getActiveSavepoints() {
        return $this->savepoints;
    }
}
// 使用封装类
$helper = new SavepointHelper($pdo);
$pdo->beginTransaction();
$helper->create('step1');
// 执行操作...
$helper->create('step2');
// 执行操作...
// 如果需要回滚
$helper->rollbackTo('step1');
$pdo->commit();
?>

最佳实践建议

<?php
class TransactionWithSavepoints {
    private $pdo;
    private $savepointCounter = 0;
    /**
     * 执行带保存点的事务
     */
    public function executeTransaction(callable $callback) {
        $this->pdo->beginTransaction();
        try {
            // 给回调传入保存点管理对象
            $result = $callback($this);
            // 提交事务
            $this->pdo->commit();
            return $result;
        } catch (Exception $e) {
            // 捕获所有异常,执行完整回滚
            $this->pdo->rollBack();
            throw $e;
        }
    }
    /**
     * 创建保存点
     */
    public function createSavepoint() {
        $name = 'sp_' . (++$this->savepointCounter);
        $this->pdo->exec("SAVEPOINT $name");
        return $name;
    }
    /**
     * 回滚到特定保存点
     */
    public function rollbackToSavepoint($name) {
        $this->pdo->exec("ROLLBACK TO SAVEPOINT $name");
        // 重置保存点计数器(可选)
        $this->savepointCounter = (int)substr($name, 3);
    }
    /**
     * 释放保存点
     */
    public function releaseSavepoint($name) {
        $this->pdo->exec("RELEASE SAVEPOINT $name");
    }
}
// 使用示例
$transaction = new TransactionWithSavepoints($pdo);
$result = $transaction->executeTransaction(function($tx) {
    // 执行第一个操作
    // ...
    // 创建保存点
    $sp = $tx->createSavepoint();
    // 执行可能会失败的操作
    try {
        // 进行风险操作
        // ...
    } catch (Exception $e) {
        // 回滚到保存点
        $tx->rollbackToSavepoint($sp);
    }
    // 继续其他操作
    // ...
    return 'transactions complete';
});
echo $result;
?>
  1. 保存点只能在事务中使用:必须在 BEGIN TRANSACTION 之后才能创建保存点

  2. 保存点命名规范:使用有意义的名称,如 sp_user_created,便于维护

  3. 回滚范围ROLLBACK TO SAVEPOINT 只回滚到指定点之后的操作,之前的操作保持不变

  4. 内存管理:及时释放不需要的保存点,避免过度使用

  5. 错误处理:始终使用 try-catch 捕获异常,确保在错误时能正确恢复

  6. 数据库支持:确保数据库版本支持保存点功能(MySQL 5.0+,PostgreSQL 8.0+ 等)

  7. 性能考虑:保存点会占用资源,不要过度使用,在必要的情况下才使用

抱歉,评论功能暂时关闭!