weaving/php-dtm

A PHP coroutine client for distributed transaction manager DTM. 分布式事务管理器 DTM 的 PHP 协程客户端

v0.2.0 2022-03-24 04:20 UTC

README

英文 | 中文

DTM Logo

Stable Version Php Version dtm-client License

PHPUnit for dtm-client Total Downloads Monthly Downloads

简介

dtm/dtm-client is the PHP client of Distributed Transaction Manager DTM. It has supported distributed transaction patterns of TCC pattern, Saga pattern, and two-phase message pattern. In communicate protocol it has supported communicate with DTM Server through HTTP protocol or gRPC protocol. Also the client can safely run in PHP-FPM and Swoole coroutine environment, and it has also make support more easier for Hyperf framework.

About DTM

DTM is an open source distributed transaction manager based on Go language, which provides the powerful function of combining transactions across languages and storage engines. DTM elegantly solves distributed transaction problems such as interface idempotent, null compensation, and transaction suspension, and also provides a distributed transaction solutions that are easy to use, high performance, and easy to scale horizontally.

优势

  • 易于启动
    • 使用零配置启动服务并提供非常简单清晰的 HTTP 接口,大大降低了分布式事务的入门难度
  • 跨编程语言
    • 适用于拥有多个语言堆栈的公司。在 Go、Python、PHP、NodeJs、Ruby、C# 等各种语言中都很方便使用
  • 易于使用
    • 开发者不再需要担心事务挂起、空补偿、接口幂等和其他问题,首个子事务屏障技术为您处理这些问题
  • 易于部署和扩展
    • 仅依赖 MySQL/Redis,易于部署、易于集群和易于水平扩展
  • 支持多种分布式事务协议
    • TCC、SAGA、XA、两阶段消息,一站式解决各种分布式事务问题

比较

In non-Java languages, there is still no mature distributed transaction manager other than DTM, so here is a comparison between DTM and Seata, the most mature open source project in Java

从上述比较的特点来看,DTM 在许多方面具有很大的优势。如果您考虑多语言支持和多存储引擎支持,那么 DTM 无疑是您的首选。

安装

通过 Composer 安装 dtm-client 非常方便

composer require dtm/dtm-client
  • 在开始使用之前,请务必启动 DTM 服务器

配置

配置文件

如果您正在使用 Hyperf 框架,在安装组件后,可以使用以下 vendor:publish 命令将配置文件发布到 ./config/autoload/dtm.php

php bin/hyperf.php vendor:publish dtm/dtm-client

如果您使用的是非 Hyperf 框架,请将 ./vendor/dtm/dtm-client/publish/dtm.php 文件复制到相应的配置目录。

use DtmClient\Constants\Protocol;
use DtmClient\Constants\DbType;

return [
    // The communication protocol between the client and the DTM Server, supports Protocol::HTTP and Protocol::GRPC
    'protocol' => Protocol::HTTP,
    // DTM Server address
    'server' => '127.0.0.1',
    // DTM Server port
    'port' => [
        'http' => 36789,
        'grpc' => 36790,
    ],
    // Sub-transaction barrier
    'barrier' => [
        // Subtransaction barrier configuration in DB mode 
        'db' => [
            'type' => DbType::MySQL
        ],
        // Subtransaction barrier configuration in Redis mode
        'redis' => [
            // Timeout for subtransaction barrier records
            'expire_seconds' => 7 * 86400,
        ],
        // Classes that apply sub-transaction barriers in non-Hyperf frameworks or without annotation usage
        'apply' => [],
    ],
    // Options of Guzzle client under HTTP protocol
    'guzzle' => [
        'options' => [],
    ],
];

配置中间件

在使用之前,您需要将 DtmClient\Middleware\DtmMiddleware 中间件配置为服务器的全局中间件。此中间件支持 PSR-15 规范,并适用于支持此规范的所有框架。有关 Hyperf 中的中间件配置,请参阅 Hyperf 文档 - 中间件 章节。

用法

dtm-client 的使用非常简单,我们提供了一个示例项目 dtm-php/dtm-sample,以帮助您更好地理解和调试。在开始使用此组件之前,强烈建议您阅读 DTM 官方文档,以获得更详细的理解。

TCC 模式

TCC 模式是一个非常流行的灵活分布式事务解决方案。TCC 的概念由三个词的首字母缩写组成:Try-Confirm-Cancel。它首次在 2007 年 Pat Helland 发表的一篇名为 Life beyond Distributed Transactions: an Apostate’s Opinion 的论文中提出。

TCC 的三个阶段

尝试阶段:尝试执行,完成所有业务检查(一致性),预留必要的业务资源(预隔离);确认阶段:如果所有分支的尝试都成功,进入确认阶段。确认实际上执行业务而不进行任何业务检查,仅使用尝试阶段预留的业务资源;取消阶段:如果所有分支的尝试中有一个失败,进入取消阶段。释放尝试阶段预留的业务资源。

如果我们想执行类似于银行间转账的业务,转账出(TransOut)和转账入(TransIn)位于不同的微服务中,一个成功完成的 TCC 事务的典型序列图如下

示例

以下展示了如何在 Hyperf 框架中使用它,其他框架类似

<?php
namespace App\Controller;

use DtmClient\TCC;
use DtmClient\TransContext;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\GetMapping;
use Throwable;

#[Controller(prefix: '/tcc')]
class TccController
{

    protected string $serviceUri = 'http://127.0.0.1:9501';

    #[Inject]
    protected TCC $tcc;

    #[GetMapping(path: 'successCase')]
    public function successCase()
    {
        try {
            
            $this->tcc->globalTransaction(function (TCC $tcc) {
                // Create call data for subtransaction A
                $tcc->callBranch(
                    // Arguments for calling the Try method
                    ['amount' => 30],
                    // URL of Try stage
                    $this->serviceUri . '/tcc/transA/try',
                    // URL of Confirm stage
                    $this->serviceUri . '/tcc/transA/confirm',
                    // URL of Cancel stage
                    $this->serviceUri . '/tcc/transA/cancel'
                );
                // Create call data for subtransaction B, and so on
                $tcc->callBranch(
                    ['amount' => 30],
                    $this->serviceUri . '/tcc/transB/try',
                    $this->serviceUri . '/tcc/transB/confirm',
                    $this->serviceUri . '/tcc/transB/cancel'
                );
            });
        } catch (Throwable $e) {
            var_dump($e->getMessage(), $e->getTraceAsString());
        }
        // Get the global transaction ID through TransContext::getGid() and return it to the client
        return TransContext::getGid();
    }
}

Saga 模式

Saga 模式是分布式事务领域最知名的解决方案之一,它也非常受大型系统欢迎。它首次出现在 Hector Garcaa-Molrna 和 Kenneth Salem 在 1987 年发表的论文 SAGAS 中。

Saga 是一个最终一致性事务,也是一个灵活事务,也称为长事务。Saga 是一系列本地事务的集合。每个本地事务更新数据库后,将发布一个消息或事件来触发 Saga 全局事务中下一个本地事务的执行。如果一个本地事务由于某些业务规则无法满足而失败,Saga 将对在失败事务之前成功提交的所有事务执行补偿操作。因此,当将 Saga 模式与 TCC 模式进行比较时,由于缺少资源预留步骤,通常需要更复杂的回滚逻辑。

Saga 的子事务拆分

例如,我们想执行类似于银行间转账的业务,将 30 美元从账户 A 转入账户 B。根据 Saga 事务的原则,我们将整个全局事务拆分为以下服务

  • 转账出(TransOut)服务,账户 A 将扣除 30 美元
  • 转账出补偿(TransOutCompensate)服务,回滚上述转账出操作,即增加账户 A 30 美元
  • 转账入(TransIn)服务,账户 B 将增加 30 美元
  • 转账入补偿(TransInCompensate)服务,回滚上述转账入操作,即账户 B 减少 30 美元

整个事务的逻辑是

执行转账出成功 => 执行转账入成功 => 全局事务完成

如果在中间发生错误,例如将资金转入账户 B 时出现错误,将调用已执行分支的补偿操作,即

执行转账出成功 => 执行转账入失败 => 执行转账入补偿成功 => 执行转账出补偿成功 => 全局事务回滚完成

以下是一个成功完成的 SAGA 事务的典型序列图

示例

以下展示了如何在 Hyperf 框架中使用它,其他框架类似

namespace App\Controller;

use DtmClient\Saga;
use DtmClient\TransContext;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\GetMapping;

#[Controller(prefix: '/saga')]
class SagaController
{

    protected string $serviceUri = 'http://127.0.0.1:9501';
    
    #[Inject]
    protected Saga $saga;

    #[GetMapping(path: 'successCase')]
    public function successCase(): string
    {
        $payload = ['amount' => 50];
        // Init Saga global transaction
        $this->saga->init();
        // Add TransOut sub-transaction
        $this->saga->add(
            $this->serviceUri . '/saga/transOut', 
            $this->serviceUri . '/saga/transOutCompensate', 
            $payload
        );
        // Add TransIn sub-transaction
        $this->saga->add(
            $this->serviceUri . '/saga/transIn', 
            $this->serviceUri . '/saga/transInCompensate', 
            $payload
        );
        // Submit Saga global transaction
        $this->saga->submit();
        // Get the global transaction ID through TransContext::getGid() and return it to the client
        return TransContext::getGid();
    }
}