怎样在PHP项目中实现TCC事务?

wen java案例 3

本文目录导读:

怎样在PHP项目中实现TCC事务?

  1. TCC 核心原理回顾
  2. 自研轻量级 TCC(适合单体/少量服务)
  3. 集成框架方案(生产级推荐)
  4. 关键注意事项
  5. 选择建议

在 PHP 项目中实现 TCC(Try-Confirm-Cancel)事务模式,通常有两种方式:自研轻量级实现(适合简单场景)或 集成分布式事务框架(适合生产级微服务架构)。

下面是详细的实现指南,包括核心原理、代码示例和注意事项。


TCC 核心原理回顾

TCC 将事务分为三个步骤:

阶段 动作 业务含义
Try 预留资源 检查业务条件并锁定资源(如冻结库存)
Confirm 确认执行 真正执行业务(如扣减库存)
Cancel 回滚取消 释放 Try 阶段预留的资源(如解冻库存)

关键设计点:

  • Try 失败的资源由调用方通过 Cancel 补偿
  • Confirm 和 Cancel 必须保证幂等(多次执行结果一致)
  • 通常需要一个事务协调器来管理状态机

自研轻量级 TCC(适合单体/少量服务)

1 数据表设计

-- 事务参与者记录表
CREATE TABLE tcc_transaction_log (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    transaction_id VARCHAR(64) NOT NULL COMMENT '全局事务ID',
    participant_id VARCHAR(64) NOT NULL COMMENT '参与者唯一标识',
    resource_type VARCHAR(32) NOT NULL COMMENT '资源类型(如: user_account)',
    resource_id VARCHAR(64) COMMENT '资源ID',
    status TINYINT DEFAULT 0 COMMENT '0:Trying, 1:Confirming, 2:Confirmed, 3:Cancelling, 4:Cancelled',
    retry_count INT DEFAULT 0,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY uk_tx_participant (transaction_id, participant_id)
);

2 业务服务示例(账户余额转账)

// 1. 账户服务 - TCC 接口
class AccountTccService
{
    private $db; // 数据库连接
    private $tccLogger; // TCC 日志表操作类
    /** Try:冻结资金 */
    public function tryFreeze(int $userId, float $amount, string $txId): bool
    {
        // 开启本地事务
        $this->db->beginTransaction();
        try {
            // 1. 检查余额是否充足
            $balance = $this->getBalance($userId);
            if ($balance < $amount) {
                throw new \Exception("余额不足");
            }
            // 2. 冻结资金(冻结字段或冻结表)
            $this->freezeBalance($userId, $amount);
            // 3. 记录 TCC 日志(Try 成功)
            $this->tccLogger->save([
                'transaction_id' => $txId,
                'participant_id' => "account:{$userId}",
                'resource_type'  => 'user_account',
                'resource_id'    => $userId,
                'status'         => 0, // Trying
            ]);
            $this->db->commit();
            return true;
        } catch (\Exception $e) {
            $this->db->rollBack();
            // 记录 Cancel 轨迹(可提前标记)
            $this->tccLogger->cancel($txId, "account:{$userId}");
            return false;
        }
    }
    /** Confirm:扣减冻结资金 */
    public function confirmFreeze(string $txId, int $userId, float $amount): bool
    {
        // 幂等检查:如果已经 Confirm,直接返回成功
        $log = $this->tccLogger->findByTxAndParticipant($txId, "account:{$userId}");
        if ($log['status'] >= 2) {
            return true; // 幂等处理
        }
        $this->db->beginTransaction();
        try {
            // 1. 实际扣减(冻结转扣减)
            $this->deductBalance($userId, $amount);
            // 2. 解冻剩余(若存在非全额冻结,需解冻多余部分)
            $this->unfreezeBalance($userId, $amount);
            // 3. 更新 TCC 日志状态为 Confirmed
            $this->tccLogger->updateStatus($txId, "account:{$userId}", 2);
            $this->db->commit();
            return true;
        } catch (\Exception $e) {
            $this->db->rollBack();
            // 记录重试或人工处理
            $this->tccLogger->markRetry($txId, "account:{$userId}");
            return false;
        }
    }
    /** Cancel:解冻资金 */
    public function cancelFreeze(string $txId, int $userId, float $amount): bool
    {
        $log = $this->tccLogger->findByTxAndParticipant($txId, "account:{$userId}");
        if ($log['status'] == 4) {
            return true; // 已取消,幂等返回
        }
        $this->db->beginTransaction();
        try {
            // 1. 解冻之前冻结的资金
            $this->unfreezeBalance($userId, $amount);
            // 2. 标记状态为 Cancelled
            $this->tccLogger->updateStatus($txId, "account:{$userId}", 4);
            $this->db->commit();
            return true;
        } catch (\Exception $e) {
            $this->db->rollBack();
            // 重试
            $this->tccLogger->markRetry($txId, "account:{$userId}");
            return false;
        }
    }
    // ... 数据库操作细节省略
}

3 事务协调器(核心调度逻辑)

class TccCoordinator
{
    private $services = []; // 参与者列表
    private $txId;
    public function executeTcc(callable $tryFunction, array $participants): bool
    {
        $this->txId = uniqid('tcc_', true);
        // 1. 执行所有 Try
        $tryResults = [];
        foreach ($participants as $index => $participant) {
            try {
                $result = $participant->try($this->txId);
                $tryResults[$index] = $result;
            } catch (\Throwable $e) {
                $tryResults[$index] = false;
                // Try 失败,立即执行已成功参与者的 Cancel
                $this->rollbackTries($tryResults, $participants);
                return false;
            }
        }
        // 2. 如果所有 Try 成功,执行 Confirm
        $confirmSuccess = true;
        foreach ($participants as $index => $participant) {
            try {
                $result = $participant->confirm($this->txId);
                if (!$result) {
                    $confirmSuccess = false;
                }
            } catch (\Throwable $e) {
                $confirmSuccess = false;
            }
            // Confirm 失败,启动补偿(Cancel)
            if (!$confirmSuccess) {
                $this->rollbackTries($tryResults, $participants);
                return false;
            }
        }
        return true;
    }
    private function rollbackTries(array $tryResults, array $participants): void
    {
        // 对每个 Try 成功的参与者执行 Cancel
        foreach ($tryResults as $index => $result) {
            if ($result === true) {
                try {
                    $participants[$index]->cancel($this->txId);
                } catch (\Throwable $e) {
                    // 失败记录日志,后续通过定时任务补偿
                    ErrorLog::log("TCC Cancel failed: " . $e->getMessage());
                }
            }
        }
    }
}

使用示例:

$coordinator = new TccCoordinator();
$accountService = new AccountTccService();
$orderService = new OrderTccService();
$success = $coordinator->executeTcc(
    null,
    [
        $accountService,   // 参与者1:账户冻结
        $orderService,     // 参与者2:订单冻结
    ]
);
if ($success) {
    echo "分布式事务成功";
} else {
    echo "事务回滚";
}

集成框架方案(生产级推荐)

对于微服务架构(如 Laravel、Symfony),推荐使用成熟的分布式事务框架:

框架 适用场景 特点
Seata (Fescar) 微服务、跨数据库、跨服务 成熟度高,支持 AT、TCC、Saga
ByteTCC Java 生态,但可适配 PHP 基于 Try-Confirm-Cancel
TCC-Transaction Java 生态 高性能,适合金融场景
自研 + 消息队列 任何语言 灵活可控,但实现复杂

在 PHP 中集成 Seata(通过 REST API)

  1. 部署 Seata Server(Java 程序):

    docker run -d --name seata-server -p 8091:8091 seataio/seata-server:1.6.1
  2. PHP 端实现 TCC 参与者(通过 HTTP API 暴露 Try/Confirm/Cancel):

    // 暴露给 Seata 调用的端点
    Route::post('/tcc/account/try', function(Request $request) {
        $service = new AccountTccService();
        $result = $service->tryFreeze(
            $request->input('userId'),
            $request->input('amount'),
            $request->input('xid')  // Seata 全局事务ID
        );
        return response()->json(['success' => $result]);
    });
    Route::post('/tcc/account/confirm', function(Request $request) {
        // ...
    });
    Route::post('/tcc/account/cancel', function(Request $request) {
        // ...
    });
  3. PHP 事务发起者(调用 Seata 全局事务 API):

    $seataServer = 'http://localhost:8091';
    // 1. 开启全局事务
    $xid = file_get_contents("{$seataServer}/api/v1/begin?applicationId=php-app&transactionServiceGroup=my_group");
    try {
        // 2. 调用各个微服务的 Try
        httpPost("service-a:8000/tcc/account/try", ['xid' => $xid, 'userId' => 1, 'amount' => 100]);
        httpPost("service-b:8000/tcc/order/try", ['xid' => $xid, 'orderId' => 'ORD2024...']);
        // 3. 提交全局事务
        file_get_contents("{$seataServer}/api/v1/commit?xid={$xid}");
    } catch (Exception $e) {
        // 4. 回滚全局事务
        file_get_contents("{$seataServer}/api/v1/rollback?xid={$xid}");
    }

关键注意事项

1 幂等设计

  • 每个 Confirm/Cancel 接口必须能重复执行而不产生副作用
  • 实现方式:
    // 基于数据库唯一索引或状态机
    public function confirm(string $xid, int $userId): bool {
        // 状态机:只有 Trying 状态才执行 Confirm
        $log = $this->tccLogRepo->findByXidAndParticipant($xid, "account:$userId");
        if ($log['status'] !== 0) {
            return true; // 已经处理过
        }
        // 执行业务...
    }

2 空回滚问题

  • 当一个参与者 Try 失败但未执行(网络超时),Cancel 仍然可能被调用
  • 解决方案:Cancel 方法应识别未执行 Try 的情况,直接返回成功

3 悬挂问题

  • Try 成功但 Confirm/Cancel 永远收不到(协调者宕机)
  • 解决方案:使用定时任务扫描 TCC 日志表,重试 Confirm/Cancel,或标记为最终失败

4 使用消息队列做异步补偿

对于高延时场景,可将 Cancel/Confirm 异步化:

// Try 成功后,发送 Confirm 消息到队列
$this->messageQueue->publish('tcc_confirm', [
    'xid' => $xid,
    'participant_id' => 'account:1',
]);
// 消费者幂等执行 Confirm

选择建议

场景 推荐方案
单体应用,少量跨库操作 自研 TCC + 本地事务表
2-4个微服务,团队熟悉 PHP 自研 TCC + HTTP/RPC + 定时补偿
微服务较多,追求可靠 集成 Seata(Java 协调器)
高并发、高一致性要求 Seata AT / TCC + Redis 锁 + 消息队列

核心要记住:TCC 的本质是业务侵入式补偿,设计好 Try/Confirm/Cancel 的幂等逻辑比代码实现更重要。

如果你能提供更具体的业务场景(如电商订单、支付对账、库存管理等),我可以帮你设计更贴合业务的数据结构和接口方案。

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