PHP 怎么PHP 回滚策略

wen PHP项目 1

本文目录导读:

PHP 怎么PHP 回滚策略

  1. 数据库事务回滚(最核心、最常用)
  2. 多步骤文件/状态回滚(手动实现)
  3. 文件系统事务(使用临时文件 + 重命名)
  4. 分布式/微服务回滚(Saga 模式)
  5. 选择最优策略

在 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 和变量快照恢复状态

核心原则

  1. 尽早失败:在业务校验完成后再提交事务。
  2. 补偿幂等:回滚操作最好是幂等的(多次执行结果一样)。
  3. 不可逆操作:如“发送邮件”无法撤回,需通过业务补偿(如发送退款/撤销邮件)。

如果你能提供更具体的业务场景(“我有10个步骤,失败后只回滚后5个”),我可以给出更精确的代码实现。

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