vhnnnbj/laravel-rt

用于调用远程API服务的分布式事务

v0.2.001 2023-11-23 06:33 UTC

README

中文文档

RT(重置事务)模式可以作为调用远程API服务的分布式事务

概述

安装版本介于laravel5.5-laravel8之间,然后安装composer包

## Composer2 version must be used
composer require windawake/laravel-reset-transaction dev-master

首先创建订单、存储、账户3个MySQL数据库实例,3个控制器,3个模型,将测试套件Transaction添加到phpunit.xml中,然后启动web服务器。这些操作只需要执行以下命令即可完成

php artisan resetTransact:create-examples && php artisan serve --host=0.0.0.0 --port=8000

在另一个终端中,使用端口8001启动web服务器

php artisan serve --host=0.0.0.0 --port=8001

最后运行测试脚本 ./vendor/bin/phpunit --testsuite=Transaction --filter=ServiceTest 运行结果如下,3个示例已通过测试。

DESKTOP:/web/linux/php/laravel/laravel62# ./vendor/bin/phpunit --testsuite=Transaction --filter=ServiceTest
Time: 219 ms, Memory: 22.00 MB

OK (3 tests, 12 assertions)

特性

  1. 开箱即用,无需重构原始项目的代码,与MySQL事务写入一致,简单易用。
  2. 遵循两阶段提交协议,这是一个强一致性事务。在高并发情况下,它支持读提交事务的隔离级别,数据一致性接近100%的MySQL xa。
  3. 由于事务被分割成多个,成为几个小事务,压力测试发现死锁比MySQL普通事务更少。
  4. 支持分布式事务嵌套,与savepoint一致。
  5. 支持避免不同业务代码并发引起的脏数据问题。
  6. 默认支持http协议的服务端接口。如果需要支持其他协议,需要重写中间件。
  7. 支持子服务的嵌套分布式事务(全球首创).
  8. 支持服务,本地事务和分布式事务的混合嵌套(全球首创)
  9. 支持3次超时重试,重复请求确保幂等性
  10. 支持go、java语言(开发中)

原理

看过电影《明日边缘》后,你会了解归档和读取文件的操作。这个分布式事务组件模仿了电影《明日边缘》的原则。重置意味着重置,即在每个请求开始时重新读取基本服务的基本文件,然后继续后续操作。最后,所有操作回滚并归档,最后一步成功执行所有归档。整个过程遵循两阶段提交协议,首先准备,然后提交。

以用户A使用招商银行卡向用户B的招商银行账户转账100元为例,以下流程图。 在右侧图片中,重置分布式事务开启后,比左侧图片多4个请求。请求4所做的是请求1-3之前所做的,然后回到原点并重新开始,最后提交事务,结束转账过程。

支持子服务的嵌套分布式事务(全球首创)

一个世界级的问题:服务A提交->B服务回滚->C服务提交->D服务提交sql。在这种情况下,ABCD都是不同的数据库。我该如何让A服务提交B服务并回滚?C服务和D服务的所有操作怎么办?

既不能解决此问题的seata也不能解决。解决问题的关键是C服务和D服务必须提交错误,不能提交正确。如果提交了,将无法恢复。

实现支持子服务的嵌套分布式事务有哪些好处?你可以让A服务成为其他服务的服务,并且它可以任意嵌套在任何层的链接中。打破之前的束缚:服务A必须是根服务,如果服务A要成为子服务,则必须更改代码。如果使用RT模式,服务A可以成为他人的服务而无需更改代码。

如何使用

vendor/windawake/laravel-reset-transaction/tests/ServiceTest.php文件为例

<?php

namespace Tests\Transaction;

use App\Models\ResetAccountModel;
use Tests\TestCase;
use Illuminate\Support\Facades\DB;
use App\Models\ResetOrderModel;
use App\Models\ResetStorageModel;
use GuzzleHttp\Client;
use Laravel\ResetTransaction\Facades\RT;

class ServiceTest extends TestCase
{
    private $baseUri = 'http://127.0.0.1:8000';
    /**
     * @var \GuzzleHttp\Client
     */
    private $client;

    protected function setUp(): void
    {
        parent::setUp();
        DB::setDefaultConnection('service_order'); //默认是订单服务
        $this->client = new Client([
            'base_uri' => $this->baseUri,
            'timeout' => 60,
        ]);
	$requestId = session_create_id();
	session()->put('rt_request_id', $requestId);
    }

    public function testCreateOrderWithCommit()
    {
        $orderCount1 = ResetOrderModel::count();
        $storageItem1 = ResetStorageModel::find(1);
        $accountItem1 = ResetAccountModel::find(1);
	// 开启RT模式分布式事务
        $transactId = RT::beginTransaction();
        $orderNo = rand(1000, 9999); // 随机订单号
        $stockQty = 2; // 占用2个库存数量
        $amount = 20.55; // 订单总金额20.55元

        ResetOrderModel::create([
            'order_no' => $orderNo,
            'stock_qty' => $stockQty,
            'amount' => $amount
        ]);
        // 请求库存服务,减库存
	$requestId = session_create_id();
        $response = $this->client->put('/api/resetStorage/1', [
            'json' => [
                'decr_stock_qty' => $stockQty
            ],
            'headers' => [
                'rt_request_id' => $requestId,
                'rt_transact_id' => $transactId,
            ]
        ]);
        $resArr1 = $this->responseToArray($response);
        $this->assertTrue($resArr1['result'] == 1, 'lack of stock'); //返回值是1,说明操作成功
        // 请求账户服务,减金额
	$requestId = session_create_id();
        $response = $this->client->put('/api/resetAccount/1', [
            'json' => [
                'decr_amount' => $amount
            ],
            'headers' => [
                'rt_request_id' => $requestId,
                'rt_transact_id' => $transactId,
            ]
        ]);
        $resArr2 = $this->responseToArray($response);
        $this->assertTrue($resArr2['result'] == 1, 'not enough money'); //返回值是1,说明操作成功
	// 提交RT模式分布式事务
        RT::commit();

        $orderCount2 = ResetOrderModel::count();
        $storageItem2 = ResetStorageModel::find(1);
        $accountItem2 = ResetAccountModel::find(1);

        $this->assertTrue(($orderCount1 + 1) == $orderCount2); //事务内创建了一个订单
        $this->assertTrue(($storageItem1->stock_qty - $stockQty) == $storageItem2->stock_qty); //事务内创建订单后需要扣减库存
        $this->assertTrue(($accountItem1->amount - $amount) == $accountItem2->amount); //事务内创建订单后需要扣减账户金额
    }

    private function responseToArray($response)
    {
        $contents = $response->getBody()->getContents();
        return json_decode($contents, true);
    }
}

联系方式

扫描二维码进入微信群。我希望更多的朋友可以相互学习,共同研究分布式事务的知识。