本文目录导读:

在 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 的幂等逻辑比代码实现更重要。
如果你能提供更具体的业务场景(如电商订单、支付对账、库存管理等),我可以帮你设计更贴合业务的数据结构和接口方案。
版权声明:足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播