本文目录导读:

在 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;
?>
-
保存点只能在事务中使用:必须在
BEGIN TRANSACTION之后才能创建保存点 -
保存点命名规范:使用有意义的名称,如
sp_user_created,便于维护 -
回滚范围:
ROLLBACK TO SAVEPOINT只回滚到指定点之后的操作,之前的操作保持不变 -
内存管理:及时释放不需要的保存点,避免过度使用
-
错误处理:始终使用 try-catch 捕获异常,确保在错误时能正确恢复
-
数据库支持:确保数据库版本支持保存点功能(MySQL 5.0+,PostgreSQL 8.0+ 等)
-
性能考虑:保存点会占用资源,不要过度使用,在必要的情况下才使用