PHP 怎么补偿事务

wen PHP项目 2

本文目录导读:

PHP 怎么补偿事务

  1. 基于数据库的事务(最基础,非真正补偿)
  2. 本地消息表(可靠消息最终一致性)
  3. TCC 模式(Try-Confirm-Cancel)
  4. 使用消息队列 + 死信队列补偿
  5. Saga 模式(长事务)
  6. 分布式事务中间件(简化开发)
  7. 总结与选型建议

在 PHP 中处理事务补偿,通常是因为分布式系统跨多个数据源操作时,单一数据库的 transaction(ACID)无法满足需求,补偿的核心思想是:既然无法保证原子性,就通过“反向操作”来达到最终一致性

以下是 PHP 中实现事务补偿的几种主流方案和实战代码:


基于数据库的事务(最基础,非真正补偿)

如果只是单库操作,直接用原生事务即可,不需要补偿,补偿只针对跨库或跨服务。

// 单库场景,不需要补偿
try {
    $pdo->beginTransaction();
    // 扣库存
    $pdo->exec("UPDATE stock SET num = num -1 WHERE id = 1");
    // 建订单
    $pdo->exec("INSERT INTO orders ...");
    $pdo->commit();
} catch (\Throwable $e) {
    $pdo->rollBack();
}

单库根本不需要补偿,rollBack 就是最好的补偿。


本地消息表(可靠消息最终一致性)

适用于业务逻辑在同一服务内但涉及多个内部操作,或者需要异步解耦的场景。

原理:

在业务数据库中创建一张 compensation_log 表(或者 message 表),本地事务直接写业务表 + 写日志表(同库同事务),后台异步脚本根据日志状态执行补偿动作。

实现示例:

// 业务表
CREATE TABLE `orders` (
  `id` INT PRIMARY KEY,
  `status` TINYINT -- 1=待支付,2=已支付,3=已取消
);
// 补偿表(记录需要补偿的操作)
CREATE TABLE `operation_log` (
  `id` INT PRIMARY KEY AUTO_INCREMENT,
  `order_id` INT,
  `operation` VARCHAR(50), -- 'decrease_stock' 或 'increase_stock'
  `status` TINYINT DEFAULT 0, -- 0=待补偿,1=已补偿
  `created_at` TIMESTAMP
);
class OrderService {
    public function createOrder($userId, $productId, $qty) {
        $pdo->beginTransaction();
        try {
            // 1. 扣库存(这里假设是远程调用,但记入日志)
            // 伪代码:$stockService->decrease($productId, $qty);
            // 如果扣库存失败,直接异常回滚
            // 2. 写订单
            $pdo->exec("INSERT INTO orders ...");
            // 3. 记录补偿日志(如果后续步骤失败,需要反向加库存)
            $stmt = $pdo->prepare("INSERT INTO operation_log (order_id, operation) VALUES (?, ?)");
            $stmt->execute([$orderId, 'increase_stock']);
            $pdo->commit();
        } catch (\Throwable $e) {
            $pdo->rollBack();
            // 如果扣库存已经通过网络成功,这里需要手动调用扣库存的反向操作
            // $stockService->increase($productId, $qty); // 直接补偿
        }
    }
}
// 异步补偿脚本(命令行定时任务运行)
class CompensationWorker {
    public function run() {
        $pdo = new PDO(...);
        // 循环查找待补偿的日志
        $logs = $pdo->query("SELECT * FROM operation_log WHERE status = 0")->fetchAll();
        foreach ($logs as $log) {
            try {
                // 根据 operation 字段执行对应的反向操作
                if ($log['operation'] === 'increase_stock') {
                    // $this->callStockService('increase', ...);
                }
                // 标记为已补偿
                $pdo->exec("UPDATE operation_log SET status = 1 WHERE id = {$log['id']}");
            } catch (\Throwable $e) {
                // 记录失败,等待下次重试(需要设置重试次数上限)
                error_log("补偿失败: " . $log['id']);
            }
        }
    }
}

TCC 模式(Try-Confirm-Cancel)

适用于对一致性要求高的核心链路(如资金、库存),需要三个接口:Try(预留资源)、Confirm(确认)、Cancel(取消)。

步骤拆解(伪代码):

// 库存服务
class StockService {
    // 阶段一:Try 预留库存
    public function tryReserve($productId, $qty) {
        // UPDATE stock SET frozen_qty = frozen_qty + $qty WHERE id = $productId AND (total - frozen_qty) >= $qty
        // 如果不成功返回 false
    }
    // 阶段二:Confirm 扣减预留
    public function confirm($productId, $qty) {
        // UPDATE stock SET total = total - $qty, frozen_qty = frozen_qty - $qty WHERE id = $productId
    }
    // 阶段三:Cancel 释放预留
    public function cancel($productId, $qty) {
        // UPDATE stock SET frozen_qty = frozen_qty - $qty WHERE id = $productId
    }
}
// 主流程调用
function createOrderWithTCC($userId, $productId, $qty) {
    $stock = new StockService();
    $order = new OrderService();
    // 1. Try 阶段
    if (!$stock->tryReserve($productId, $qty)) {
        throw new Exception("库存不足");
    }
    try {
        // 2. 执行业务(创建订单)
        $order->create($userId, $productId, $qty);
        // 3. Confirm 阶段
        $stock->confirm($productId, $qty);
    } catch (\Throwable $e) {
        // 4. 异常 -> Cancel 阶段(释放资源)
        $stock->cancel($productId, $qty);
        throw $e;
    }
}

注意:TCC 的 Cancel 和 Confirm 必须是幂等的(因为网络超时会重试)。


使用消息队列 + 死信队列补偿

适用于业务解耦场景,典型支付流程。

实现思路:

  • 业务完成写库后,通过 RabbitMQKafka 发送异步消息。
  • 消费者收到消息后执行业务,如果消费失败且重试几次后仍失败,消息进入死信队列(DLX)。
  • 专门写一个补偿消费者监听死信队列,对失败的订单进行退款/关单操作。
// 生产者
$mq->send('order.create', ['order_id' => 123]);
// 消费者(处理库存扣减)
class StockConsumer {
    public function handle($msg) {
        try {
            $this->decreaseStock($msg->order_id);
            $msg->ack(); // 确认
        } catch (\Throwable $e) {
            // 重试三次后,进入死信队列
            if ($msg->retries >= 3) {
                $msg->dead(); // 发送至死信队列
            } else {
                $msg->retry();
            }
        }
    }
}

补偿消费者监控死信队列,执行 cancelOrderrefund


Saga 模式(长事务)

适用于跨服务、流程长的业务(比如预订旅游:订酒店 -> 订机票 -> 扣款)。

通过把一个长流程拆成多个子事务,每个子事务都有对应的逆向操作,使用状态机推进流程,哪个步骤失败就执行之前所有步骤的逆向操作。

class BookingSaga {
    public function create(array $steps) {
        $done = [];
        foreach ($steps as $step) {
            try {
                $step->execute();
                $done[] = $step;
            } catch (\Throwable $e) {
                // 补偿:反向执行之前成功的步骤
                foreach (array_reverse($done) as $d) {
                    $d->compensate();
                }
                throw $e;
            }
        }
    }
}
// 具体步骤
class BookHotel {
    public function execute() { /* 请求酒店API */ }
    public function compensate() { /* 取消酒店 */ }
}
class BookFlight {
    public function execute() { /* 请求机票API */ }
    public function compensate() { /* 取消机票 */ }
}

分布式事务中间件(简化开发)

如果你不想手动写这么多补偿代码,可以使用现成的 PHP 分布式事务中间件:

框架/中间件 说明
DTM (Distributed Transaction Manager) 目前比较火的 Go/PHP 均可用的分布式事务管理器,支持 TCC、Saga、消息队列,提供 HTTP API,PHP 端只需回调接口。
Seata (Fescar) 阿里开源,支持 AT 模式(自动补偿,通过解析 SQL 生成逆向 SQL),PHP 有 SDK 可调用,但主要生态在 Java。
Hyperf Saga 如果在用 Swoole 或 Hyperf 框架,它自带 Saga 分布式事务组件。

总结与选型建议

场景 推荐方案
单体应用,单数据库 直接使用 PDO->rollBack()不需要补偿
单体应用,跨多个内部表 本地消息表 + 定时任务补偿。
涉及远程 API(如第三方支付、库存) TCC 模式(由你控制反向调用) 或 消息队列+死信
跨服务长流程(状态机复杂) Saga 模式(让流程状态驱动补偿) 或引入 DTM 中间件。
不想手写轮子 使用 DTMSeata 中间件。

核心要点:补偿操作必须满足幂等性(重复执行多次结果一致),并且要有重试机制(失败后定时重试)和监控报警(超过重试次数进入人工处理队列)。

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