本文目录导读:

在 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)
-
部署 Seata Server(Java 程序):
docker run -d --name seata-server -p 8091:8091 seataio/seata-server:1.6.1
-
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) { // ... }); -
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 的幂等逻辑比代码实现更重要。
如果你能提供更具体的业务场景(如电商订单、支付对账、库存管理等),我可以帮你设计更贴合业务的数据结构和接口方案。