本文目录导读:

- 基于数据库的事务(最基础,非真正补偿)
- 本地消息表(可靠消息最终一致性)
- TCC 模式(Try-Confirm-Cancel)
- 使用消息队列 + 死信队列补偿
- Saga 模式(长事务)
- 分布式事务中间件(简化开发)
- 总结与选型建议
在 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 必须是幂等的(因为网络超时会重试)。
使用消息队列 + 死信队列补偿
适用于业务解耦场景,典型支付流程。
实现思路:
- 业务完成写库后,通过
RabbitMQ或Kafka发送异步消息。 - 消费者收到消息后执行业务,如果消费失败且重试几次后仍失败,消息进入死信队列(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();
}
}
}
}
补偿消费者监控死信队列,执行 cancelOrder 或 refund。
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 中间件。 |
| 不想手写轮子 | 使用 DTM 或 Seata 中间件。 |
核心要点:补偿操作必须满足幂等性(重复执行多次结果一致),并且要有重试机制(失败后定时重试)和监控报警(超过重试次数进入人工处理队列)。