本文目录导读:

在 PHP 中实现“回滚策略”,主要取决于你是在处理数据库事务、文件操作还是多步骤业务流程。
以下是最常见场景的详细回滚实现方式:
数据库事务回滚(最核心、最常用)
这是最标准的回滚场景,用于确保数据库操作的原子性(要么全部成功,要么全部失败)。
使用 PDO(推荐)
<?php
try {
$pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8mb4', 'root', '');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// 步骤1: 开启事务
$pdo->beginTransaction();
// 步骤2: 执行多个SQL
$pdo->exec("UPDATE accounts SET balance = balance - 100 WHERE id = 1");
$pdo->exec("UPDATE accounts SET balance = balance + 100 WHERE id = 2");
// 步骤3: 检查业务逻辑(例如余额不能为负)
$stmt = $pdo->query("SELECT balance FROM accounts WHERE id = 1");
$balance = $stmt->fetchColumn();
if ($balance < 0) {
throw new Exception("余额不足,触发回滚");
}
// 步骤4: 全部成功 -> 提交
$pdo->commit();
echo "转账成功!";
} catch (Exception $e) {
// 步骤5: 出错 -> 回滚
if (isset($pdo) && $pdo->inTransaction()) {
$pdo->rollBack();
}
echo "转账失败:" . $e->getMessage();
}
?>
关键点:
beginTransaction()+rollBack()必须在同一个连接上执行,且rollBack()只回滚当前连接未提交的事务。
使用 MySQLi
<?php
$mysqli = new mysqli('localhost', 'root', '', 'test');
$mysqli->autocommit(FALSE); // 关闭自动提交,相当于开启事务
try {
$mysqli->query("UPDATE accounts SET balance = balance - 100 WHERE id = 1");
if ($mysqli->affected_rows === 0) {
throw new Exception("更新失败");
}
$mysqli->query("UPDATE accounts SET balance = balance + 100 WHERE id = 2");
$mysqli->commit();
echo "成功";
} catch (Exception $e) {
$mysqli->rollback();
echo "回滚: " . $e->getMessage();
}
?>
多步骤文件/状态回滚(手动实现)
当业务涉及多个外部系统(如:发短信、写文件、调用API),你需要手动记录已执行的步骤,失败时反向撤销。
示例:用户注册(插入数据库 + 发送邮件 + 创建目录)
<?php
class UserRegistrationService {
private $steps = []; // 记录已执行的步骤
private $pdo;
public function register($username) {
try {
$this->pdo->beginTransaction();
// 步骤1: 写入数据库
$stmt = $this->pdo->prepare("INSERT INTO users (username) VALUES (?)");
$stmt->execute([$username]);
$userId = $this->pdo->lastInsertId();
$this->steps[] = 'database';
// 步骤2: 创建用户目录
$dir = "./uploads/{$userId}";
if (!mkdir($dir, 0755, true)) {
throw new Exception("创建目录失败");
}
$this->steps[] = 'directory';
// 步骤3: 发送欢迎邮件(假设可能失败)
if (!$this->sendWelcomeEmail($username)) {
throw new Exception("邮件发送失败");
}
$this->steps[] = 'email';
$this->pdo->commit();
return true;
} catch (Exception $e) {
// 回滚策略:反向撤销已执行的步骤
$this->rollbackPreviousSteps($userId ?? null);
if ($this->pdo->inTransaction()) {
$this->pdo->rollBack();
}
throw $e;
}
}
private function rollbackPreviousSteps($userId) {
// 按相反顺序回滚
$steps = array_reverse($this->steps);
foreach ($steps as $step) {
switch ($step) {
case 'email':
// 无法真正撤销已发送的邮件,记录日志
error_log("无法回滚已发送的邮件");
break;
case 'directory':
if ($userId) {
$dir = "./uploads/{$userId}";
if (is_dir($dir)) {
rmdir($dir); // 删除目录
}
}
break;
case 'database':
// 数据库回滚由 PDO->rollBack() 负责
break;
}
}
}
private function sendWelcomeEmail($username) {
// 模拟发送邮件,可能失败
return rand(0, 1) === 1; // 50% 概率失败
}
}
// 使用
$service = new UserRegistrationService();
try {
$service->register('john_doe');
echo "注册成功";
} catch (Exception $e) {
echo "注册失败:" . $e->getMessage();
}
?>
文件系统事务(使用临时文件 + 重命名)
这是一种流行的原子性文件操作策略,特别适合处理大文件或配置更新。
<?php
$targetFile = '/var/www/html/config.php';
$tempFile = $targetFile . '.tmp';
// 步骤1: 先写入临时文件
$data = "<?php\n\$config = ['status' => 'updated'];\n";
file_put_contents($tempFile, $data);
// 步骤2: 验证临时文件完整性(例如校验hash)
if (md5_file($tempFile) !== 'expected_md5') {
unlink($tempFile); // 删除临时文件 = 回滚
die("校验失败,已回滚");
}
// 步骤3: 原子重命名(同一文件系统内,rename是原子的)
if (rename($tempFile, $targetFile)) {
echo "成功";
} else {
unlink($tempFile); // 重命名失败,删除临时文件
echo "失败";
}
?>
分布式/微服务回滚(Saga 模式)
在微服务架构中,跨服务事务无法使用数据库本地事务,这是理论和代码实现相对复杂的场景,常用模式是 Saga(长事务):
- Choreography(编排式):每个服务执行后发出事件,下一个服务监听执行;失败时由触发补偿事件。
- Orchestration(编舞式):一个协调器负责调用每个服务,并记录状态,失败时调用每个服务的回滚 API。
简单实现(编舞式)
<?php
class SagaCoordinator {
private $executedServices = [];
public function execute(ServiceInterface ...$services) {
foreach ($services as $service) {
try {
$service->execute();
$this->executedServices[] = $service;
} catch (Exception $e) {
// 回滚所有已执行的服务
foreach (array_reverse($this->executedServices) as $executedService) {
try {
$executedService->rollback();
} catch (Exception $rollbackEx) {
error_log("回滚失败: {$rollbackEx->getMessage()}");
}
}
throw $e;
}
}
}
}
// 使用
$coordinator = new SagaCoordinator();
$coordinator->execute(
new PaymentService(),
new InventoryService(),
new NotificationService()
);
?>
选择最优策略
| 场景 | 推荐策略 |
|---|---|
| 单数据库操作 | 使用 PDO / MySQLi 的 beginTransaction() + rollBack()(最标准) |
| 单服务器文件+数据库 | 手动记录步骤,按反向顺序撤销(第2节) |
| 配置文件/大文件更新 | 使用临时文件 + rename() 原子操作(第3节) |
| 微服务跨系统事务 | 使用消息队列(补偿事件)或 Saga 模式(第4节) |
| 内存中操作 | 使用 try/finally 和变量快照恢复状态 |
核心原则:
- 尽早失败:在业务校验完成后再提交事务。
- 补偿幂等:回滚操作最好是幂等的(多次执行结果一样)。
- 不可逆操作:如“发送邮件”无法撤回,需通过业务补偿(如发送退款/撤销邮件)。
如果你能提供更具体的业务场景(“我有10个步骤,失败后只回滚后5个”),我可以给出更精确的代码实现。