├── examples ├── test │ └── Transaction │ │ ├── data.txt │ │ ├── BenchmarkTest.php │ │ └── ServiceTest.php ├── Model │ ├── ResetAccountModel.php │ ├── ResetStorageModel.php │ └── ResetOrderModel.php ├── Controller │ ├── ResetTransactionController.php │ ├── ResetAccountController.php │ ├── ResetOrderController.php │ ├── ResetStorageController.php │ ├── ResetAccountTestController.php │ └── ResetOrderTestController.php └── config │ └── rt_database.php ├── src ├── Exception │ └── ResetTransactionException.php ├── Database │ ├── MySqlProcessor.php │ ├── MySqlGrammar.php │ └── ResetMySqlConnection.php ├── Facades │ ├── RT.php │ └── ResetTransaction.php ├── ConfigProvider.php ├── Middleware │ ├── ServiceOrderMiddleware.php │ ├── ServiceAccountMiddleware.php │ ├── ServiceStorageMiddleware.php │ └── DistributeTransactMiddleware.php ├── Core │ └── OverrideDb.php └── Command │ └── CreateExamplesCommand.php ├── composer.json └── README.md /examples/test/Transaction/data.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Exception/ResetTransactionException.php: -------------------------------------------------------------------------------- 1 | {$name}(...$arguments); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/ConfigProvider.php: -------------------------------------------------------------------------------- 1 | [ 25 | \Hyperf\DbConnection\Db::class => \Windawake\HyperfResetTransaction\Core\OverrideDb::class, 26 | ], 27 | 'commands' => [ 28 | CreateExamplesCommand::class, 29 | ], 30 | 'databases' => $this->getRtDatabases() 31 | ]; 32 | // overide MySqlConnection 33 | Connection::resolverFor('mysql', function ($connection, $database, $prefix, $configArr) { 34 | return new ResetMySqlConnection($connection, $database, $prefix, $configArr); 35 | }); 36 | 37 | return $configArr; 38 | } 39 | 40 | private function getRtDatabases() 41 | { 42 | $rt = include BASE_PATH.'/config/autoload/rt_database.php'; 43 | if ($rt) { 44 | return array_merge($rt['center']['connections'], $rt['service_connections']); 45 | } 46 | 47 | return []; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Database/MySqlGrammar.php: -------------------------------------------------------------------------------- 1 | container = $container; 39 | } 40 | 41 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 42 | { 43 | Context::set('rt_connection_defaultName', 'service_order'); 44 | 45 | $response = $handler->handle($request); 46 | 47 | return $response; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Middleware/ServiceAccountMiddleware.php: -------------------------------------------------------------------------------- 1 | container = $container; 39 | } 40 | 41 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 42 | { 43 | Context::set('rt_connection_defaultName', 'service_account'); 44 | 45 | $response = $handler->handle($request); 46 | 47 | return $response; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Middleware/ServiceStorageMiddleware.php: -------------------------------------------------------------------------------- 1 | container = $container; 39 | } 40 | 41 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 42 | { 43 | Context::set('rt_connection_defaultName', 'service_storage'); 44 | 45 | $response = $handler->handle($request); 46 | 47 | return $response; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /examples/Controller/ResetTransactionController.php: -------------------------------------------------------------------------------- 1 | request->input('transact_id'); 32 | $transactRollback = $this->request->input('transact_rollback', []); 33 | $code = 1; 34 | 35 | RT::centerCommit($transactId, $transactRollback); 36 | 37 | return ['code' => $code, 'transact_id' => $transactId]; 38 | } 39 | 40 | /** 41 | * @PostMapping(path="rollback") 42 | */ 43 | public function rollback() 44 | { 45 | // 46 | $transactId = $this->request->input('transact_id'); 47 | $transactRollback = $this->request->input('transact_rollback', []); 48 | $code = 1; 49 | 50 | RT::centerRollback($transactId, $transactRollback); 51 | 52 | return ['code' => $code, 'transactId' => $transactId]; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Database/ResetMySqlConnection.php: -------------------------------------------------------------------------------- 1 | checkResult = $checkResult; 35 | return $this; 36 | } 37 | 38 | /** 39 | * Detect the return value when committing the transaction 40 | * 41 | * @return bool 42 | */ 43 | public function getCheckResult() 44 | { 45 | return $this->checkResult; 46 | } 47 | 48 | /** 49 | * Get the default query grammar instance. 50 | * 51 | * @return \Illuminate\Database\Query\Grammars\MySqlGrammar 52 | */ 53 | protected function getDefaultQueryGrammar() 54 | { 55 | return $this->withTablePrefix(new MySqlGrammar); 56 | } 57 | 58 | /** 59 | * Get the default post processor instance. 60 | * 61 | * @return \Illuminate\Database\Query\Processors\MySqlProcessor 62 | */ 63 | protected function getDefaultPostProcessor() 64 | { 65 | return new MySqlProcessor; 66 | } 67 | 68 | /** 69 | * Run an SQL statement and get the number of rows affected. 70 | * 71 | * @param string $query 72 | * @param array $bindings 73 | * @return int 74 | */ 75 | public function affectingStatement($query, $bindings = []): int 76 | { 77 | $result = parent::affectingStatement($query, $bindings); 78 | 79 | RT::saveQuery($query, $bindings, $result, $this->checkResult); 80 | $this->checkResult = false; 81 | 82 | return (int) $result; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Core/OverrideDb.php: -------------------------------------------------------------------------------- 1 | container = $container; 56 | } 57 | 58 | public function __connection($pool = null): ConnectionInterface 59 | { 60 | $resolver = $this->container->get(ConnectionResolverInterface::class); 61 | if ($pool == null) { 62 | $pool = Context::get('rt_connection_defaultName'); 63 | } 64 | 65 | $connection = $resolver->connection($pool); 66 | Context::set('rt_connection', $connection); 67 | 68 | return $connection; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Middleware/DistributeTransactMiddleware.php: -------------------------------------------------------------------------------- 1 | container = $container; 39 | } 40 | 41 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 42 | { 43 | $requestId = $request->getHeaderLine('rt_request_id'); 44 | $transactId = $request->getHeaderLine('rt_transact_id'); 45 | 46 | if ($transactId) { 47 | if (!$requestId) { 48 | throw new ResetTransactionException('rt_request_id cannot be null'); 49 | } 50 | Context::set('rt_request_id', $requestId); 51 | // $item = DB::connection('rt_center')->table('reset_transact_req')->where('request_id', $requestId)->first(); 52 | // if ($item) { 53 | // $data = json_decode($item->response, true); 54 | // return Response::json($data); 55 | // } 56 | 57 | RT::middlewareBeginTransaction($transactId); 58 | } 59 | 60 | $response = $handler->handle($request); 61 | 62 | $requestId = $request->getHeaderLine('rt_request_id'); 63 | $transactId = $request->getHeaderLine('rt_transact_id'); 64 | // $transactIdArr = explode('-', $transactId); 65 | if ($transactId && $response->isSuccessful()) { 66 | RT::middlewareRollback(); 67 | // DB::connection('rt_center')->table('reset_transact_req')->insert([ 68 | // 'transact_id' => $transactIdArr[0], 69 | // 'request_id' => $requestId, 70 | // 'response' => $response->getBody()->getContents(), 71 | // ]); 72 | } 73 | 74 | return $response; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /examples/Controller/ResetAccountController.php: -------------------------------------------------------------------------------- 1 | request->all()); 55 | } 56 | 57 | /** 58 | * @GetMapping(path="{id:\d+}") 59 | */ 60 | public function show($id) 61 | { 62 | // 63 | $item = ResetAccountModel::find($id); 64 | return $item ?? []; 65 | } 66 | 67 | /** 68 | * @PutMapping(path="{id:\d+}") 69 | * 70 | 71 | * @param int $id 72 | * 73 | 74 | */ 75 | public function update($id) 76 | { 77 | // 78 | $item = ResetAccountModel::findOrFail($id); 79 | if ($this->request->has('decr_amount')) { 80 | $decrAmount = (float) $this->request->input('decr_amount'); 81 | $ret = ResetAccountModel::where('id', $id)->where('amount', '>', $decrAmount)->decrement('amount', $decrAmount); 82 | } else { 83 | $ret = ResetAccountModel::where('id', $id)->update($this->request->all()); 84 | } 85 | 86 | return ['result' => $ret]; 87 | } 88 | 89 | /** 90 | * @DeleteMapping(path="{id:\d+}") 91 | */ 92 | public function destroy($id) 93 | { 94 | // 95 | $item = ResetAccountModel::findOrFail($id); 96 | $ret = $item->delete(); 97 | return ['result' => $ret]; 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /examples/Controller/ResetOrderController.php: -------------------------------------------------------------------------------- 1 | request->has('status')) { 47 | $query->where('status', $this->request->input('status')); 48 | } 49 | return $query->paginate(); 50 | } 51 | 52 | /** 53 | * @PostMapping(path="") 54 | */ 55 | public function store() 56 | { 57 | // 58 | $orderNo = $this->request->input('order_no'); 59 | $item = ResetOrderModel::where('order_no', $orderNo)->first(); 60 | if ($item) { 61 | throw new Exception("order_no is exist"); 62 | } 63 | return ResetOrderModel::create($this->request->all()); 64 | } 65 | 66 | /** 67 | * @GetMapping(path="{id:\d+}") 68 | */ 69 | public function show($id) 70 | { 71 | // 72 | $item = ResetOrderModel::find($id); 73 | return $item ?? []; 74 | } 75 | 76 | /** 77 | * @PutMapping(path="{id:\d+}") 78 | */ 79 | public function update($id) 80 | { 81 | // 82 | $item = ResetOrderModel::findOrFail($id); 83 | $ret = $item->update($this->request->all()); 84 | return ['result' => $ret]; 85 | } 86 | 87 | /** 88 | * @DeleteMapping(path="{id:\d+}") 89 | */ 90 | public function destroy($id) 91 | { 92 | // 93 | $item = ResetOrderModel::findOrFail($id); 94 | $ret = $item->delete(); 95 | return ['result' => $ret]; 96 | } 97 | 98 | /** 99 | * @PutMapping(path="updateOrCreate/{id:\d+}") 100 | */ 101 | public function updateOrCreate($id) 102 | { 103 | // 104 | $attr = ['id' => $id]; 105 | $item = ResetOrderModel::updateOrCreate($attr, $this->request->all()); 106 | return $item; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /examples/Controller/ResetStorageController.php: -------------------------------------------------------------------------------- 1 | request->all()); 53 | } 54 | 55 | /** 56 | * @GetMapping(path="{id:\d+}") 57 | */ 58 | public function show($id) 59 | { 60 | // 61 | $item = ResetStorageModel::find($id); 62 | return $item ?? []; 63 | } 64 | 65 | /** 66 | * @PutMapping(path="{id:\d+}") 67 | */ 68 | public function update($id) 69 | { 70 | // 71 | $item = ResetStorageModel::findOrFail($id); 72 | if ($this->request->has('decr_stock_qty')) { 73 | $decrQty = (float) $this->request->input('decr_stock_qty'); 74 | $ret = ResetStorageModel::where('id', $id)->where('stock_qty', '>', $decrQty)->decrement('stock_qty', $decrQty); 75 | } else { 76 | $ret = ResetStorageModel::where('id', $id)->update($this->request->all()); 77 | } 78 | return ['result' => $ret]; 79 | } 80 | 81 | /** 82 | * @DeleteMapping(path="{id:\d+}") 83 | */ 84 | public function destroy($id) 85 | { 86 | // 87 | $item = ResetStorageModel::findOrFail($id); 88 | $ret = $item->delete(); 89 | return ['result' => $ret]; 90 | } 91 | 92 | /** 93 | * @PutMapping(path="/updateWithCommit/{id:\d+}") 94 | */ 95 | public function updateWithCommit($id) 96 | { 97 | $item = ResetStorageModel::findOrFail($id); 98 | Db::beginTransaction(); 99 | 100 | if ($this->request->has('decr_stock_qty')) { 101 | $decrQty = (float) $this->request->input('decr_stock_qty'); 102 | $ret = $item->where('stock_qty', '>', $decrQty)->decrement('stock_qty', $decrQty); 103 | } else { 104 | $ret = $item->update($this->request->all()); 105 | } 106 | 107 | Db::commit(); 108 | 109 | return ['result' => $ret]; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /examples/Controller/ResetAccountTestController.php: -------------------------------------------------------------------------------- 1 | 300, 48 | ]); 49 | $orderNo = session_create_id(); 50 | $stockQty = rand(1, 5); 51 | $amount = rand(1, 50)/10; 52 | Db::beginTransaction(); 53 | 54 | $client->post('http://127.0.0.1:9502/api/resetOrder', [ 55 | 'json' => [ 56 | 'order_no' => $orderNo, 57 | 'stock_qty' => $stockQty, 58 | 'amount' => $amount 59 | ] 60 | ]); 61 | 62 | $response = $client->put('http://127.0.0.1:9502/api/resetStorage/1', [ 63 | 'json' => [ 64 | 'decr_stock_qty' => $stockQty 65 | ] 66 | ]); 67 | 68 | $resArr = json_decode($response->getBody()->getContents(), true); 69 | 70 | $rowCount = ResetAccountModel::where('id', 1)->where('amount', '>', $amount)->decrement('amount', $amount); 71 | 72 | $result = $resArr['result'] && $rowCount>0; 73 | Db::commit(); 74 | 75 | return ['code' => 1, 'result' => $result]; 76 | } 77 | 78 | /** 79 | * @PostMapping(path="createOrderWithRt") 80 | */ 81 | public function createOrderWithRt() 82 | { 83 | $client = new Client([ 84 | 'timeout' => 300, 85 | ]); 86 | $orderNo = session_create_id(); 87 | $stockQty = rand(1, 5); 88 | $amount = rand(1, 50)/10; 89 | $transactId = RT::beginTransaction(); 90 | 91 | $client->post('http://127.0.0.1:9502/api/resetOrder', [ 92 | 'json' => [ 93 | 'order_no' => $orderNo, 94 | 'stock_qty' => $stockQty, 95 | 'amount' => $amount 96 | ], 97 | 'headers' => [ 98 | 'rt_request_id' => session_create_id(), 99 | 'rt_transact_id' => $transactId, 100 | ] 101 | ]); 102 | 103 | $response = $client->put('http://127.0.0.1:9502/api/resetStorage/1', [ 104 | 'json' => [ 105 | 'decr_stock_qty' => $stockQty 106 | ], 107 | 'headers' => [ 108 | 'rt_request_id' => session_create_id(), 109 | 'rt_transact_id' => $transactId, 110 | ] 111 | ]); 112 | 113 | $resArr = json_decode($response->getBody()->getContents(), true); 114 | 115 | $rowCount = ResetAccountModel::where('id', 1)->where('amount', '>', $amount)->decrement('amount', $amount); 116 | $result = $resArr['result'] && $rowCount>0; 117 | 118 | RT::commit(); 119 | 120 | return ['code' => 1, 'result' => $result]; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /examples/config/rt_database.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'commit_url' => 'http://127.0.0.1:9503/api/resetTransaction/commit', 15 | 'rollback_url' => 'http://127.0.0.1:9503/api/resetTransaction/rollback', 16 | 'connections' => [ 17 | 'rt_center' => [ 18 | 'connection_name' => 'rt_center', 19 | 'driver' => env('DB_DRIVER', 'mysql'), 20 | 'host' => env('DB_HOST', 'localhost'), 21 | 'database' => 'rt_center', 22 | 'port' => env('DB_PORT', 3306), 23 | 'username' => env('DB_USERNAME', 'root'), 24 | 'password' => env('DB_PASSWORD', ''), 25 | 'charset' => env('DB_CHARSET', 'utf8'), 26 | 'collation' => env('DB_COLLATION', 'utf8_unicode_ci'), 27 | 'prefix' => env('DB_PREFIX', ''), 28 | 'pool' => [ 29 | 'min_connections' => 1, 30 | 'max_connections' => 300, 31 | 'connect_timeout' => 10.0, 32 | 'wait_timeout' => 30, 33 | 'heartbeat' => -1, 34 | 'max_idle_time' => (float) env('DB_MAX_IDLE_TIME', 60), 35 | ], 36 | 'commands' => [ 37 | 'gen:model' => [ 38 | 'path' => 'app/Model', 39 | 'force_casts' => true, 40 | 'inheritance' => 'Model', 41 | ], 42 | ], 43 | ], 44 | ] 45 | 46 | ], 47 | 'service_connections' => [ 48 | 'service_order' => [ 49 | 'connection_name' => 'service_order', 50 | 'driver' => env('DB_DRIVER', 'mysql'), 51 | 'host' => env('DB_HOST', 'localhost'), 52 | 'database' => 'service_order', 53 | 'port' => env('DB_PORT', 3306), 54 | 'username' => env('DB_USERNAME', 'root'), 55 | 'password' => env('DB_PASSWORD', ''), 56 | 'charset' => env('DB_CHARSET', 'utf8'), 57 | 'collation' => env('DB_COLLATION', 'utf8_unicode_ci'), 58 | 'prefix' => env('DB_PREFIX', ''), 59 | 'pool' => [ 60 | 'min_connections' => 1, 61 | 'max_connections' => 300, 62 | 'connect_timeout' => 10.0, 63 | 'wait_timeout' => 30, 64 | 'heartbeat' => -1, 65 | 'max_idle_time' => (float) env('DB_MAX_IDLE_TIME', 60), 66 | ], 67 | 'commands' => [ 68 | 'gen:model' => [ 69 | 'path' => 'app/Model', 70 | 'force_casts' => true, 71 | 'inheritance' => 'Model', 72 | ], 73 | ], 74 | ], 75 | 'service_storage' => [ 76 | 'connection_name' => 'service_storage', 77 | 'driver' => env('DB_DRIVER', 'mysql'), 78 | 'host' => env('DB_HOST', 'localhost'), 79 | 'database' => 'service_storage', 80 | 'port' => env('DB_PORT', 3306), 81 | 'username' => env('DB_USERNAME', 'root'), 82 | 'password' => env('DB_PASSWORD', ''), 83 | 'charset' => env('DB_CHARSET', 'utf8'), 84 | 'collation' => env('DB_COLLATION', 'utf8_unicode_ci'), 85 | 'prefix' => env('DB_PREFIX', ''), 86 | 'pool' => [ 87 | 'min_connections' => 1, 88 | 'max_connections' => 300, 89 | 'connect_timeout' => 10.0, 90 | 'wait_timeout' => 30, 91 | 'heartbeat' => -1, 92 | 'max_idle_time' => (float) env('DB_MAX_IDLE_TIME', 60), 93 | ], 94 | 'commands' => [ 95 | 'gen:model' => [ 96 | 'path' => 'app/Model', 97 | 'force_casts' => true, 98 | 'inheritance' => 'Model', 99 | ], 100 | ], 101 | ], 102 | 'service_account' => [ 103 | 'connection_name' => 'service_account', 104 | 'driver' => env('DB_DRIVER', 'mysql'), 105 | 'host' => env('DB_HOST', 'localhost'), 106 | 'database' => 'service_account', 107 | 'port' => env('DB_PORT', 3306), 108 | 'username' => env('DB_USERNAME', 'root'), 109 | 'password' => env('DB_PASSWORD', ''), 110 | 'charset' => env('DB_CHARSET', 'utf8'), 111 | 'collation' => env('DB_COLLATION', 'utf8_unicode_ci'), 112 | 'prefix' => env('DB_PREFIX', ''), 113 | 'pool' => [ 114 | 'min_connections' => 1, 115 | 'max_connections' => 300, 116 | 'connect_timeout' => 10.0, 117 | 'wait_timeout' => 30, 118 | 'heartbeat' => -1, 119 | 'max_idle_time' => (float) env('DB_MAX_IDLE_TIME', 60), 120 | ], 121 | 'commands' => [ 122 | 'gen:model' => [ 123 | 'path' => 'app/Model', 124 | 'force_casts' => true, 125 | 'inheritance' => 'Model', 126 | ], 127 | ], 128 | ], 129 | ] 130 | 131 | ]; -------------------------------------------------------------------------------- /examples/test/Transaction/BenchmarkTest.php: -------------------------------------------------------------------------------- 1 | checkAb(); 30 | } 31 | 32 | public function testDeadlock01() 33 | { 34 | // $con = Db::connection('service_order'); 35 | 36 | $shellOne = "ab -n 12 -c 4 {$this->urlOne}/resetOrderTest/deadlockWithLocal"; 37 | $shellTwo = "ab -n 12 -c 4 {$this->urlTwo}/resetOrderTest/deadlockWithLocal"; 38 | 39 | $shell = sprintf("%s & %s", $shellOne, $shellTwo); 40 | exec($shell, $output, $resultCode); 41 | 42 | // $sql = "SHOW ENGINE INNODB STATUS"; 43 | // $ret = $con->select($sql); 44 | // Log::info($ret); 45 | 46 | } 47 | 48 | public function testDeadlock02() 49 | { 50 | $shellOne = "ab -n 12 -c 4 {$this->urlOne}/resetOrderTest/deadlockWithRt"; 51 | $shellTwo = "ab -n 12 -c 4 {$this->urlTwo}/resetOrderTest/deadlockWithRt"; 52 | 53 | $shell = sprintf("%s & %s", $shellOne, $shellTwo); 54 | exec($shell, $output, $resultCode); 55 | } 56 | 57 | public function testBatchCreate01() 58 | { 59 | $count1 = ResetOrderModel::count(); 60 | $dataPath = __DIR__.'/data.txt'; 61 | 62 | $shellOne = "ab -n 1000 -c 100 -p '{$dataPath}' {$this->urlOne}/resetOrderTest/orderWithLocal"; 63 | $shell = sprintf("%s", $shellOne); 64 | passthru($shell, $resultCode); 65 | $count2 = ResetOrderModel::count(); 66 | 67 | $this->assertTrue($count2 - $count1 == 1000); 68 | 69 | $this->assertTrue(true); 70 | } 71 | 72 | public function testBatchCreate02() 73 | { 74 | $count1 = ResetOrderModel::count(); 75 | $dataPath = __DIR__.'/data.txt'; 76 | 77 | $shellOne = "ab -n 1000 -c 100 -p '{$dataPath}' {$this->urlOne}/resetOrderTest/orderWithRt"; 78 | $shell = sprintf("%s", $shellOne); 79 | passthru($shell, $resultCode); 80 | 81 | $count2 = ResetOrderModel::count(); 82 | 83 | $this->assertTrue($count2 - $count1 == 1000); 84 | } 85 | 86 | public function testBatchCreate03() 87 | { 88 | ResetOrderModel::where('id', '<=', 10)->delete(); 89 | sleep(6); 90 | $shellOne = "ab -n 100 -c 10 {$this->urlOne}/resetOrderTest/disorderWithLocal"; 91 | $shellTwo = "ab -n 100 -c 10 {$this->urlTwo}/resetOrderTest/disorderWithLocal"; 92 | 93 | $shell = sprintf("%s & %s", $shellOne, $shellTwo); 94 | exec($shell, $output, $resultCode); 95 | } 96 | 97 | public function testBatchCreate04() 98 | { 99 | ResetOrderModel::where('id', '<=', 10)->delete(); 100 | sleep(6); 101 | $shellOne = "ab -n 100 -c 10 {$this->urlOne}/resetOrderTest/disorderWithRt"; 102 | $shellTwo = "ab -n 100 -c 10 {$this->urlTwo}/resetOrderTest/disorderWithRt"; 103 | 104 | $shell = sprintf("%s & %s", $shellOne, $shellTwo); 105 | exec($shell, $output, $resultCode); 106 | } 107 | 108 | public function testBatchCreate05() 109 | { 110 | $total1 = ResetOrderModel::count(); 111 | $amountSum1 = ResetOrderModel::sum('amount'); 112 | $stockSum1 = ResetOrderModel::sum('stock_qty'); 113 | $amount1 = ResetAccountModel::where('id', 1)->value('amount'); 114 | $stock1 = ResetStorageModel::where('id', 1)->value('stock_qty'); 115 | 116 | $dataPath = __DIR__.'/data.txt'; 117 | 118 | $reqNums = 1000; 119 | $conNums = 100; 120 | $shellOne = "ab -n {$reqNums} -c {$conNums} -p '{$dataPath}' {$this->urlOne}/resetAccountTest/createOrderWithLocal"; 121 | $shell = sprintf("%s", $shellOne); 122 | passthru($shell, $resultCode); 123 | 124 | 125 | 126 | $total2 = ResetOrderModel::count(); 127 | $amountSum2 = ResetOrderModel::sum('amount'); 128 | $stockSum2 = ResetOrderModel::sum('stock_qty'); 129 | $amount2 = ResetAccountModel::where('id', 1)->value('amount'); 130 | $stock2 = ResetStorageModel::where('id', 1)->value('stock_qty'); 131 | 132 | $this->assertTrue(($total2 - $total1) == $reqNums); 133 | $this->assertTrue(abs(($amount1 - $amount2) - ($amountSum2 - $amountSum1)) < 0.001); 134 | $this->assertTrue(($stock1 - $stock2) == ($stockSum2 - $stockSum1)); 135 | 136 | } 137 | 138 | public function testBatchCreate06() 139 | { 140 | $total1 = ResetOrderModel::count(); 141 | $amountSum1 = ResetOrderModel::sum('amount'); 142 | $stockSum1 = ResetOrderModel::sum('stock_qty'); 143 | $amount1 = ResetAccountModel::where('id', 1)->value('amount'); 144 | $stock1 = ResetStorageModel::where('id', 1)->value('stock_qty'); 145 | 146 | $dataPath = __DIR__.'/data.txt'; 147 | 148 | $reqNums = 1000; 149 | $conNums = 100; 150 | $shellOne = "ab -n {$reqNums} -c {$conNums} -p '{$dataPath}' {$this->urlOne}/resetAccountTest/createOrderWithRt"; 151 | $shell = sprintf("%s", $shellOne); 152 | passthru($shell, $resultCode); 153 | 154 | 155 | 156 | $total2 = ResetOrderModel::count(); 157 | $amountSum2 = ResetOrderModel::sum('amount'); 158 | $stockSum2 = ResetOrderModel::sum('stock_qty'); 159 | $amount2 = ResetAccountModel::where('id', 1)->value('amount'); 160 | $stock2 = ResetStorageModel::where('id', 1)->value('stock_qty'); 161 | 162 | $this->assertTrue(($total2 - $total1) == $reqNums); 163 | $this->assertTrue(abs(($amount1 - $amount2) - ($amountSum2 - $amountSum1)) < 0.001); 164 | $this->assertTrue(($stock1 - $stock2) == ($stockSum2 - $stockSum1)); 165 | 166 | } 167 | 168 | private function checkAb() 169 | { 170 | $ret = System::exec('which abc'); 171 | if (empty($ret['output'])) { 172 | throw new \InvalidArgumentException('ab not exists.'); 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /examples/Controller/ResetOrderTestController.php: -------------------------------------------------------------------------------- 1 | request->getPort()) % 2; 48 | for ($i = $s; $i < 3; $i++) { 49 | $id = $i % 2 + 1; 50 | $attrs = ['id' => $id]; 51 | $values = ['order_no' => session_create_id()]; 52 | 53 | ResetOrderModel::updateOrCreate($attrs, $values); 54 | usleep(rand(1, 200) * 1000); 55 | } 56 | Db::commit(); 57 | return ['code' => 1]; 58 | } 59 | 60 | /** 61 | * @PostMapping(path="deadlockWithRt") 62 | */ 63 | public function deadlockWithRt() 64 | { 65 | $transactId = RT::beginTransaction(); 66 | $s = ($this->request->getPort()) % 2; 67 | for ($i = $s; $i < 3; $i++) { 68 | $id = $i % 2 + 1; 69 | // $attrs = ['id' => $id]; 70 | // $values = ['order_no' => session_create_id()]; 71 | // ResetOrderModel::updateOrCreate($attrs, $values); 72 | 73 | $client = new Client([ 74 | 'base_uri' => 'http://127.0.0.1:8002', 75 | 'timeout' => 60, 76 | ]); 77 | $client->put('/api/resetOrderTest/updateOrCreate/'.$id, [ 78 | 'json' =>['order_no' => session_create_id()], 79 | 'headers' => [ 80 | 'rt_request_id' => session_create_id(), 81 | 'rt_transact_id' => $transactId, 82 | 'rt_connection' => 'service_order' 83 | ] 84 | ]); 85 | 86 | usleep(rand(1, 200) * 1000); 87 | } 88 | RT::commit(); 89 | return ['code' => 1]; 90 | } 91 | 92 | /** 93 | * @PostMapping(path="orderCommit") 94 | */ 95 | public function orderCommit() 96 | { 97 | // Db::beginTransaction(); 98 | RT::beginTransaction(); 99 | $orderNo = session_create_id(); 100 | $stockQty = rand(1, 5); 101 | $amount = rand(1, 50) / 10; 102 | ResetOrderModel::create(['order_no' => $orderNo, 'stock_qty' => $stockQty, 'amount' => $amount]); 103 | sleep(6); 104 | // Db::commit(); 105 | RT::commit(); 106 | return ['code' => 1]; 107 | } 108 | 109 | /** 110 | * @PostMapping(path="orderRollback") 111 | */ 112 | public function orderRollback() 113 | { 114 | // Db::beginTransaction(); 115 | // RT::beginTransaction(); 116 | // $orderNo = session_create_id(); 117 | // $stockQty = rand(1, 5); 118 | // $amount = rand(1, 50) / 10; 119 | // ResetOrderModel::create(['order_no' => $orderNo, 'stock_qty' => $stockQty, 'amount' => $amount]); 120 | Db::rollback(); 121 | // RT::rollback(); 122 | return ['code' => 1]; 123 | } 124 | 125 | /** 126 | * @PostMapping(path="orderWithLocal") 127 | */ 128 | public function orderWithLocal() 129 | { 130 | Db::beginTransaction(); 131 | $orderNo = session_create_id(); 132 | $stockQty = rand(1, 5); 133 | $amount = rand(1, 50)/10; 134 | 135 | ResetOrderModel::create([ 136 | 'order_no' => $orderNo, 137 | 'stock_qty' => $stockQty, 138 | 'amount' => $amount 139 | ]); 140 | 141 | 142 | Db::commit(); 143 | 144 | for ($i = 0; $i < 10; $i++) { 145 | ResetOrderModel::first(); 146 | } 147 | 148 | return ['code' => 1]; 149 | } 150 | 151 | /** 152 | * @PostMapping(path="orderWithRt") 153 | */ 154 | public function orderWithRt() 155 | { 156 | // $logger = ApplicationContext::getContainer()->get(LoggerFactory::class)->get('sql'); 157 | 158 | RT::beginTransaction(); 159 | $orderNo = session_create_id(); 160 | $stockQty = rand(1, 5); 161 | $amount = rand(1, 50)/10; 162 | 163 | ResetOrderModel::create([ 164 | 'order_no' => $orderNo, 165 | 'stock_qty' => $stockQty, 166 | 'amount' => $amount 167 | ]); 168 | 169 | // $logger->info('orderWithRt'); 170 | RT::commit(); 171 | return ['code' => 1]; 172 | } 173 | 174 | /** 175 | * @PostMapping(path="disorderWithLocal") 176 | */ 177 | public function disorderWithLocal() 178 | { 179 | Db::beginTransaction(); 180 | usleep(rand(1, 200) * 1000); 181 | $orderNo = session_create_id(); 182 | $stockQty = rand(1, 5); 183 | $amount = rand(1, 50)/10; 184 | $status = rand(1, 3); 185 | 186 | $item = ResetOrderModel::updateOrCreate([ 187 | 'id' => rand(1, 10), 188 | ], [ 189 | 'order_no' => $orderNo, 190 | 'stock_qty' => $stockQty, 191 | 'amount' => $amount, 192 | 'status' => $status, 193 | ]); 194 | 195 | 196 | $item = ResetOrderModel::find(rand(1, 10)); 197 | if ($item) { 198 | $item->delete(); 199 | } 200 | 201 | if (rand(0,1) == 0) { 202 | ResetOrderModel::where('status', $status)->update(['stock_qty' => rand(1, 5)]); 203 | } 204 | 205 | Db::commit(); 206 | return ['code' => 1]; 207 | } 208 | 209 | /** 210 | * @PostMapping(path="disorderWithRt") 211 | */ 212 | public function disorderWithRt() 213 | { 214 | RT::beginTransaction(); 215 | usleep(rand(1, 200) * 1000); 216 | $orderNo = session_create_id(); 217 | $stockQty = rand(1, 5); 218 | $amount = rand(1, 50)/10; 219 | $status = rand(1, 3); 220 | 221 | $item = ResetOrderModel::updateOrCreate([ 222 | 'id' => rand(1, 10), 223 | ], [ 224 | 'order_no' => $orderNo, 225 | 'stock_qty' => $stockQty, 226 | 'amount' => $amount, 227 | 'status' => $status, 228 | ]); 229 | 230 | 231 | $item = ResetOrderModel::find(rand(1, 10)); 232 | if ($item) { 233 | $item->delete(); 234 | } 235 | 236 | if (rand(0,1) == 0) { 237 | ResetOrderModel::where('status', $status)->update(['stock_qty' => rand(1, 5)]); 238 | } 239 | 240 | RT::commit(); 241 | return ['code' => 1]; 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /src/Command/CreateExamplesCommand.php: -------------------------------------------------------------------------------- 1 | container = $container; 36 | $this->filesystem = $container->get(Filesystem::class); 37 | } 38 | 39 | public function handle() 40 | { 41 | // 42 | try { 43 | Db::getDoctrineSchemaManager(); 44 | } catch (\Throwable $ex) { 45 | $this->error($ex->getMessage(). '. try: composer require "doctrine/dbal"'); 46 | die; 47 | } 48 | 49 | $this->addFileToApp(); 50 | $this->addTableToDatabase(); 51 | $this->addTestsuitToPhpunit(); 52 | 53 | $this->info('Example created successfully!'); 54 | } 55 | 56 | /** 57 | * rewrite phpunit.xml 58 | * 59 | * @return void 60 | */ 61 | private function addTestsuitToPhpunit() 62 | { 63 | $content = file_get_contents(BASE_PATH.'/phpunit.xml'); 64 | $xml = new \SimpleXMLElement($content); 65 | $hasTransaction = false; 66 | 67 | foreach ($xml->testsuites->testsuite as $testsuite) { 68 | if ($testsuite->attributes()->name == 'Transaction') { 69 | $hasTransaction = true; 70 | } 71 | } 72 | 73 | if ($hasTransaction == false) { 74 | $testsuite = $xml->testsuites->addChild('testsuite'); 75 | $testsuite->addAttribute('name', 'Transaction'); 76 | $directory = $testsuite->addChild('directory', './test/Transaction'); 77 | $directory->addAttribute('suffix', 'Test.php'); 78 | 79 | $domxml = new \DOMDocument('1.0'); 80 | $domxml->preserveWhiteSpace = false; 81 | $domxml->formatOutput = true; 82 | $domxml->loadXML($xml->asXML()); 83 | $domxml->save(BASE_PATH.'/phpunit.xml'); 84 | } 85 | } 86 | 87 | /** 88 | * db 89 | * 90 | * @return void 91 | */ 92 | private function addTableToDatabase() 93 | { 94 | $transactTable = 'reset_transact'; 95 | $transactSqlTable = 'reset_transact_sql'; 96 | $transactReqTable = 'reset_transact_req'; 97 | $orderTable = 'reset_order'; 98 | $storageTable = 'reset_storage'; 99 | $accountTable = 'reset_account'; 100 | 101 | $orderService = 'service_order'; 102 | $storageService = 'service_storage'; 103 | $accountService = 'service_account'; 104 | $rtCenter = 'rt_center'; 105 | 106 | $serviceMap = [ 107 | $orderService => [ 108 | $orderTable 109 | ], 110 | $storageService => [ 111 | $storageTable, 112 | ], 113 | $accountService => [ 114 | $accountTable 115 | ], 116 | $rtCenter => [ 117 | $transactTable, $transactSqlTable, $transactReqTable, 118 | ] 119 | ]; 120 | 121 | $manager = Db::getDoctrineSchemaManager(); 122 | $dbList = $manager->listDatabases(); 123 | 124 | foreach ($serviceMap as $service => $tableList) { 125 | if (!in_array($service, $dbList)) { 126 | $manager->createDatabase($service); 127 | } 128 | 129 | foreach ($tableList as $table) { 130 | if ($table == $transactTable) { 131 | $fullTable = $service . '.' . $transactTable; 132 | Schema::dropIfExists($fullTable); 133 | Schema::create($fullTable, function (Blueprint $table) { 134 | $table->bigIncrements('id'); 135 | $table->string('transact_id', 32); 136 | $table->tinyInteger('action')->default(0); 137 | $table->text('transact_rollback'); 138 | $table->dateTime('created_at')->useCurrent(); 139 | $table->unique('transact_id'); 140 | }); 141 | } 142 | 143 | if ($table == $transactSqlTable) { 144 | $fullTable = $service . '.' . $transactSqlTable; 145 | Schema::dropIfExists($fullTable); 146 | Schema::create($fullTable, function (Blueprint $table) { 147 | $table->bigIncrements('id'); 148 | $table->string('request_id', 32); 149 | $table->string('transact_id', 512); 150 | $table->tinyInteger('transact_status')->default(0); 151 | $table->string('connection', 32); 152 | $table->text('sql'); 153 | $table->integer('result')->default(0); 154 | $table->tinyInteger('check_result')->default(0); 155 | $table->dateTime('created_at')->useCurrent(); 156 | $table->index('request_id'); 157 | $table->index('transact_id'); 158 | }); 159 | } 160 | 161 | if ($table == $transactReqTable) { 162 | $fullTable = $service . '.' . $transactReqTable; 163 | Schema::dropIfExists($fullTable); 164 | Schema::create($fullTable, function (Blueprint $table) { 165 | $table->bigIncrements('id'); 166 | $table->string('request_id', 32); 167 | $table->string('transact_id', 32); 168 | $table->text('response'); 169 | $table->dateTime('created_at')->useCurrent(); 170 | $table->unique('request_id'); 171 | $table->index('transact_id'); 172 | }); 173 | } 174 | 175 | if ($table == $orderTable) { 176 | $fullTable = $service . '.' . $orderTable; 177 | Schema::dropIfExists($fullTable); 178 | Schema::create($fullTable, function (Blueprint $table) { 179 | $table->increments('id'); 180 | $table->string('order_no')->default(''); 181 | $table->integer('stock_qty')->default(0); 182 | $table->decimal('amount')->default(0); 183 | $table->tinyInteger('status')->default(0); 184 | $table->unique('order_no'); 185 | }); 186 | } 187 | 188 | if ($table == $storageTable) { 189 | $fullTable = $service . '.' . $storageTable; 190 | Schema::dropIfExists($fullTable); 191 | Schema::create($fullTable, function (Blueprint $table) { 192 | $table->increments('id'); 193 | $table->integer('stock_qty')->default(0); 194 | }); 195 | Db::unprepared("insert into {$fullTable} values(1, 10000)"); 196 | } 197 | 198 | if ($table == $accountTable) { 199 | $fullTable = $service . '.' . $accountTable; 200 | Schema::dropIfExists($fullTable); 201 | Schema::create($fullTable, function (Blueprint $table) { 202 | $table->increments('id'); 203 | $table->decimal('amount')->default(0); 204 | }); 205 | Db::unprepared("insert into {$fullTable} values(1, 100000)"); 206 | } 207 | } 208 | } 209 | } 210 | 211 | private function addFileToApp() 212 | { 213 | $this->filesystem->copyDirectory(__DIR__ . '/../../examples/Controller', BASE_PATH.'/app/Controller'); 214 | $this->filesystem->copyDirectory(__DIR__ . '/../../examples/Model', BASE_PATH.'/app/Model'); 215 | $this->filesystem->copyDirectory(__DIR__ . '/../../examples/config', BASE_PATH.'/config/autoload'); 216 | $this->filesystem->copyDirectory(__DIR__ . '/../../examples/test', BASE_PATH.'/test'); 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /examples/test/Transaction/ServiceTest.php: -------------------------------------------------------------------------------- 1 | client = new Client([ 43 | 'base_uri' => $this->baseUri, 44 | 'timeout' => 300, 45 | ]); 46 | 47 | $requestId = session_create_id(); 48 | Context::set('rt_request_id', $requestId); 49 | 50 | // $config = ApplicationContext::getContainer()->get(ConfigInterface::class); 51 | // $databases = $config->get('databases'); 52 | // $databases['default'] = $databases['service_order']; 53 | // $config->set('databases', $databases); 54 | 55 | $resolver = ApplicationContext::getContainer()->get(ConnectionResolverInterface::class); 56 | $resolver->setDefaultConnection('service_order'); 57 | } 58 | 59 | // public function testRtDemo01() 60 | // { 61 | // $orderNo = rand(1000, 9999); // 随机订单号 62 | // $stockQty = 2; // 占用2个库存数量 63 | // $amount = 20.55; // 订单总金额20.55元 64 | // RT::beginTransaction(); 65 | 66 | // // Db::table('reset_order')->insertGetId([ 67 | // // 'order_no' => $orderNo, 68 | // // 'stock_qty' => $stockQty, 69 | // // 'amount' => $amount 70 | // // ], 'id'); 71 | 72 | // ResetOrderModel::create([ 73 | // 'order_no' => $orderNo, 74 | // 'stock_qty' => $stockQty, 75 | // 'amount' => $amount 76 | // ]); 77 | 78 | // RT::commitTest(); 79 | // // RT::commit(); 80 | // $this->assertTrue(true); 81 | // } 82 | 83 | public function testCreateOrderWithRollback() 84 | { 85 | $orderCount1 = ResetOrderModel::count(); 86 | $storageItem1 = Db::connection('service_storage')->table('reset_storage')->find(1); 87 | $accountItem1 = Db::connection('service_account')->table('reset_account')->find(1); 88 | 89 | $transactId = RT::beginTransaction(); 90 | $orderNo = rand(1000, 9999); // 随机订单号 91 | $stockQty = 2; // 占用2个库存数量 92 | $amount = 20.55; // 订单总金额20.55元 93 | 94 | ResetOrderModel::create([ 95 | 'order_no' => $orderNo, 96 | 'stock_qty' => $stockQty, 97 | 'amount' => $amount 98 | ]); 99 | 100 | $requestId = session_create_id(); 101 | // 请求库存服务,减库存 102 | $response = $this->client->put('/api/resetStorage/1', [ 103 | 'json' => [ 104 | 'decr_stock_qty' => $stockQty 105 | ], 106 | 'headers' => [ 107 | 'rt_request_id' => $requestId, 108 | 'rt_transact_id' => $transactId, 109 | ] 110 | ]); 111 | $resArr1 = $this->responseToArray($response); 112 | $this->assertTrue($resArr1['result'] == 1, 'lack of stock'); //返回值是1,说明操作成功 113 | 114 | $requestId = session_create_id(); 115 | // 请求账户服务,减金额 116 | $response = $this->client->put('/api/resetAccount/1', [ 117 | 'json' => [ 118 | 'decr_amount' => $amount 119 | ], 120 | 'headers' => [ 121 | 'rt_request_id' => $requestId, 122 | 'rt_transact_id' => $transactId, 123 | ] 124 | ]); 125 | $resArr2 = $this->responseToArray($response); 126 | $this->assertTrue($resArr2['result'] == 1, 'not enough money'); //返回值是1,说明操作成功 127 | 128 | RT::rollBack(); 129 | 130 | $orderCount2 = ResetOrderModel::count(); 131 | $storageItem2 = Db::connection('service_storage')->table('reset_storage')->find(1); 132 | $accountItem2 = Db::connection('service_account')->table('reset_account')->find(1); 133 | 134 | $this->assertTrue($orderCount1 == $orderCount2); 135 | $this->assertTrue($storageItem1->stock_qty == $storageItem2->stock_qty); 136 | $this->assertTrue($accountItem1->amount == $accountItem2->amount); 137 | } 138 | 139 | public function testCreateOrderWithCommit() 140 | { 141 | $orderCount1 = ResetOrderModel::count(); 142 | $storageItem1 = ResetStorageModel::find(1); 143 | $accountItem1 = ResetAccountModel::find(1); 144 | 145 | $transactId = RT::beginTransaction(); 146 | $orderNo = rand(1000, 9999); // 随机订单号 147 | $stockQty = 2; // 占用2个库存数量 148 | $amount = 20.55; // 订单总金额20.55元 149 | 150 | ResetOrderModel::create([ 151 | 'order_no' => $orderNo, 152 | 'stock_qty' => $stockQty, 153 | 'amount' => $amount 154 | ]); 155 | $requestId = session_create_id(); 156 | 157 | // 请求库存服务,减库存 158 | $response = $this->client->put('/api/resetStorage/1', [ 159 | 'json' => [ 160 | 'decr_stock_qty' => $stockQty 161 | ], 162 | 'headers' => [ 163 | 'rt_request_id' => $requestId, 164 | 'rt_transact_id' => $transactId, 165 | ] 166 | ]); 167 | $resArr1 = $this->responseToArray($response); 168 | $this->assertTrue($resArr1['result'] == 1, 'lack of stock'); //返回值是1,说明操作成功 169 | 170 | $requestId = session_create_id(); 171 | // 请求账户服务,减金额 172 | $response = $this->client->put('/api/resetAccount/1', [ 173 | 'json' => [ 174 | 'decr_amount' => $amount 175 | ], 176 | 'headers' => [ 177 | 'rt_request_id' => $requestId, 178 | 'rt_transact_id' => $transactId, 179 | ] 180 | ]); 181 | $resArr2 = $this->responseToArray($response); 182 | $this->assertTrue($resArr2['result'] == 1, 'not enough money'); //返回值是1,说明操作成功 183 | 184 | RT::commit(); 185 | 186 | $orderCount2 = ResetOrderModel::count(); 187 | $storageItem2 = ResetStorageModel::find(1); 188 | $accountItem2 = ResetAccountModel::find(1); 189 | 190 | $this->assertTrue(($orderCount1 + 1) == $orderCount2); 191 | $this->assertTrue($storageItem1->stock_qty - $stockQty == $storageItem2->stock_qty); 192 | $this->assertTrue(abs($accountItem1->amount - $amount - $accountItem2->amount) < 0.001); 193 | } 194 | 195 | public function testNestedTransaction() 196 | { 197 | for ($id = 11; $id <= 13; $id++) { 198 | $orderNo = rand(1000, 9999); // 随机订单号 199 | 200 | ResetOrderModel::updateOrCreate([ 201 | 'id' => $id, 202 | ], [ 203 | 'order_no' => $orderNo, 204 | ]); 205 | } 206 | 207 | $status = 11; 208 | 209 | $txId = RT::beginTransaction(); 210 | $this->client->put('api/resetOrder/11', [ 211 | 'json' => [ 212 | 'order_no' => 'aaa', 213 | 'status' => $status, 214 | ], 215 | 'headers' => [ 216 | 'rt_request_id' => session_create_id(), 217 | 'rt_transact_id' => $txId, 218 | ] 219 | ]); 220 | $txId2 = RT::beginTransaction(); 221 | $this->client->put('api/resetOrder/12', [ 222 | 'json' => [ 223 | 'order_no' => 'bbb', 224 | 'status' => $status, 225 | ], 226 | 'headers' => [ 227 | 'rt_request_id' => session_create_id(), 228 | 'rt_transact_id' => $txId2, 229 | ] 230 | ]); 231 | 232 | $txId3 = RT::beginTransaction(); 233 | $this->client->put('api/resetOrder/13', [ 234 | 'json' => [ 235 | 'order_no' => 'ccc', 236 | 'status' => $status, 237 | ], 238 | 'headers' => [ 239 | 'rt_request_id' => session_create_id(), 240 | 'rt_transact_id' => $txId3, 241 | ] 242 | ]); 243 | 244 | $response = $this->client->get('api/resetOrder', [ 245 | 'json' => [ 246 | 'status' => $status, 247 | ], 248 | 'headers' => [ 249 | 'rt_request_id' => session_create_id(), 250 | 'rt_transact_id' => $txId3, 251 | ] 252 | ]); 253 | $resArr = $this->responseToArray($response); 254 | // 3层事务内,有3个订单修改了状态 255 | $this->assertTrue($resArr['total'] == 3); 256 | 257 | RT::commit(); 258 | 259 | RT::rollBack(); 260 | 261 | RT::commit(); 262 | 263 | $response = $this->client->get('api/resetOrder', [ 264 | 'json' => [ 265 | 'status' => $status, 266 | ], 267 | ]); 268 | $resArr = $this->responseToArray($response); 269 | // 3层事务内,有3个订单修改了状态,但是第2层事务回滚了, 只有第1层事务成功提交 270 | $this->assertTrue($resArr['total'] == 1); 271 | } 272 | 273 | private function responseToArray($response) 274 | { 275 | $contents = $response->getBody()->getContents(); 276 | return json_decode($contents, true); 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 组件简介 2 | 基于分布式事务RT模式实现,适用于hyperf框架的组件。 3 | 4 | ## 如何使用 5 | 在hyperf框架里,把门面Db换成RT就能实现分布式事务。 6 | ```php 7 | use Hyperf\DbConnection\Db; 8 | use Windawake\HyperfResetTransaction\Facades\RT; 9 | 10 | Db::beginTransaction(); 11 | ... 12 | Db::commit(); 13 | 14 | #换成 15 | RT::beginTransaction(); 16 | ... 17 | RT::commit(); 18 | ``` 19 | 20 | ## 快速预览 21 | 第一步,在hyperf框架根目录下安装composer组件 22 | ```shell 23 | ## 必须使用composer2版本 24 | composer require windawake/hyperf-reset-transaction dev-master 25 | ``` 26 | 第二步,在`./config/autoload/server.php`文件,默认使用9501端口配置,然后增加9502,9503端口的配置 27 | ```php 28 | 'servers' => [ 29 | [ 30 | 'name' => 'http', 31 | 'type' => Server::SERVER_HTTP, 32 | 'host' => '0.0.0.0', 33 | 'port' => 9501, 34 | 'sock_type' => SWOOLE_SOCK_TCP, 35 | 'callbacks' => [ 36 | Event::ON_REQUEST => [Hyperf\HttpServer\Server::class, 'onRequest'], 37 | ], 38 | ], 39 | [ 40 | 'name' => 'http', 41 | 'type' => Server::SERVER_HTTP, 42 | 'host' => '0.0.0.0', 43 | 'port' => 9502, 44 | 'sock_type' => SWOOLE_SOCK_TCP, 45 | 'callbacks' => [ 46 | Event::ON_REQUEST => [Hyperf\HttpServer\Server::class, 'onRequest'], 47 | ], 48 | ], 49 | [ 50 | 'name' => 'http', 51 | 'type' => Server::SERVER_HTTP, 52 | 'host' => '0.0.0.0', 53 | 'port' => 9503, 54 | 'sock_type' => SWOOLE_SOCK_TCP, 55 | 'callbacks' => [ 56 | Event::ON_REQUEST => [Hyperf\HttpServer\Server::class, 'onRequest'], 57 | ], 58 | ] 59 | ], 60 | ``` 61 | 第三步,删除`runtime`文件夹,然后创建order,storage,account3个mysql数据库实例,3个控制器,3个model,在phpunit.xml增加testsuite Transaction,然后启动web服务器。这些操作只需要执行下面命令全部完成 62 | ```shell 63 | rm -rf ./runtime && php ./bin/hyperf.php resetTransact:create-examples && php ./bin/hyperf.php start 64 | ``` 65 | 66 | 最后一步,运行测试脚本 ` 67 | composer test -- --testsuite=Transaction --filter=ServiceTest 68 | `运行结果如下所示,3个例子测试通过。 69 | ```shell 70 | DESKTOP:/web/linux/php/hyperf/hyperf22# composer test -- --testsuite=Transaction --filter=ServiceTest 71 | Time: 00:00.596, Memory: 18.00 MB 72 | 73 | OK (3 tests, 12 assertions) 74 | ``` 75 | 76 | ## 功能特性 77 | 1. 开箱即用,不需要重构原有项目的代码,与mysql事务写法一致,简单易用。 78 | 2. 两段提交的强一致性事务,高并发下,支持读已提交的事务隔离级别,数据一致性100%接近mysql xa。 79 | 3. 性能超过seata AT模式,由于事务拆分成多个,变成了几个小事务,压测发现比mysql普通事务更少发生死锁。 80 | 4. 支持分布式事务嵌套,与savepoint一致效果。 81 | 5. 支持避免不同业务代码并发造成脏数据的问题。 82 | 6. 默认支持http协议的服务化接口,想要支持其它协议则需要重写中间件。 83 | 7. [支持子服务嵌套分布式事务(全球首创)](#支持子服务嵌套分布式事务(全球首创))。 84 | 8. 支持服务,本地事务和分布式事务混合嵌套(全球首创) 85 | 9. 支持超时3次重试,重复请求保证幂等性 86 | 10. 支持go,java语言(开发中) 87 | 88 | 对比阿里seata AT模式,有什么优点?请阅读 https://learnku.com/articles/63797 89 | 90 | ## 测试报告 91 | 92 | 本地电脑:i5-9400F 24G内存 wsl ubuntu 93 | 94 | - 压测场景: 95 | 96 | 1)创建一个简单的订单 1000请求 100并发 97 | 98 | 2)订单服务创建一个订单,然后库存服务扣减库存,最后账户服务扣减金额 1000请求 100并发 99 | 100 | - 报告总结: 101 | 102 | 1)使用RT模式,创建一个订单的消耗性能跟普通事务创建一个订单+7条简单sql语句差不多 103 | 104 | 2)一个完整的创建订单是包含订单服务,库存服务和账户服务。使用RT模式,qps从109下降到53,性能大约是不使用分布式事务的1/2 105 | 106 | - 压测前准备: 107 | 108 | 做测试之前需要设置mysql最大连接数为3000: 109 | 110 | ```sql 111 | set global max_connections=3000; 112 | ``` 113 | 114 | **压测创建一个订单 + 7条简单订单查询** 115 | 116 | 加7条简单的sql语句,是因为RT分布式事务,后面处理的逻辑大约有7条sql,这样方便比较。 117 | 118 | ```shell 119 | root@DESKTOP-VQOELJ5:/web/linux/php/hyperf/hyperf22# composer test -- --testsuite=Transaction --filter=testBatchCreate01 120 | 121 | Completed 100 requests 122 | Completed 200 requests 123 | Completed 300 requests 124 | Completed 400 requests 125 | Completed 500 requests 126 | Completed 600 requests 127 | Completed 700 requests 128 | Completed 800 requests 129 | Completed 900 requests 130 | Completed 1000 requests 131 | Finished 1000 requests 132 | . 1 / 1 (100%) 133 | This is ApacheBench, Version 2.3 <$Revision: 1843412 $> 134 | Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ 135 | Licensed to The Apache Software Foundation, http://www.apache.org/ 136 | 137 | Benchmarking 127.0.0.1 (be patient) 138 | 139 | 140 | Server Software: Hyperf 141 | Server Hostname: 127.0.0.1 142 | Server Port: 9501 143 | 144 | Document Path: /api/resetOrderTest/orderWithLocal 145 | Document Length: 10 bytes 146 | 147 | Concurrency Level: 100 148 | Time taken for tests: 5.863 seconds 149 | Complete requests: 1000 150 | Failed requests: 0 151 | Total transferred: 153000 bytes 152 | Total body sent: 161000 153 | HTML transferred: 10000 bytes 154 | Requests per second: 170.58 [#/sec] (mean) 155 | Time per request: 586.251 [ms] (mean) 156 | Time per request: 5.863 [ms] (mean, across all concurrent requests) 157 | Transfer rate: 25.49 [Kbytes/sec] received 158 | 26.82 kb/s sent 159 | 52.31 kb/s total 160 | 161 | Connection Times (ms) 162 | min mean[+/-sd] median max 163 | Connect: 0 0 0.4 0 2 164 | Processing: 26 562 77.5 582 682 165 | Waiting: 24 562 77.5 581 682 166 | Total: 26 563 77.2 582 683 167 | 168 | Percentage of the requests served within a certain time (ms) 169 | 50% 582 170 | 66% 585 171 | 75% 588 172 | 80% 590 173 | 90% 593 174 | 95% 596 175 | 98% 601 176 | 99% 626 177 | 100% 683 (longest request) 178 | 179 | 180 | Time: 00:05.894, Memory: 16.00 MB 181 | 182 | OK (1 test, 2 assertions) 183 | ``` 184 | 185 | **压测开启RT分布式事务创建一个订单** 186 | 187 | ``` 188 | root@DESKTOP-VQOELJ5:/web/linux/php/hyperf/hyperf22# composer test -- --testsuite=Transaction --filter=testBatchCreate02 189 | 190 | Completed 100 requests 191 | Completed 200 requests 192 | Completed 300 requests 193 | Completed 400 requests 194 | Completed 500 requests 195 | Completed 600 requests 196 | Completed 700 requests 197 | Completed 800 requests 198 | Completed 900 requests 199 | Completed 1000 requests 200 | Finished 1000 requests 201 | . 1 / 1 (100%) 202 | This is ApacheBench, Version 2.3 <$Revision: 1843412 $> 203 | Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ 204 | Licensed to The Apache Software Foundation, http://www.apache.org/ 205 | 206 | Benchmarking 127.0.0.1 (be patient) 207 | 208 | 209 | Server Software: Hyperf 210 | Server Hostname: 127.0.0.1 211 | Server Port: 9501 212 | 213 | Document Path: /api/resetOrderTest/orderWithRt 214 | Document Length: 10 bytes 215 | 216 | Concurrency Level: 100 217 | Time taken for tests: 6.006 seconds 218 | Complete requests: 1000 219 | Failed requests: 0 220 | Total transferred: 153000 bytes 221 | Total body sent: 158000 222 | HTML transferred: 10000 bytes 223 | Requests per second: 166.51 [#/sec] (mean) 224 | Time per request: 600.557 [ms] (mean) 225 | Time per request: 6.006 [ms] (mean, across all concurrent requests) 226 | Transfer rate: 24.88 [Kbytes/sec] received 227 | 25.69 kb/s sent 228 | 50.57 kb/s total 229 | 230 | Connection Times (ms) 231 | min mean[+/-sd] median max 232 | Connect: 0 0 0.6 0 3 233 | Processing: 55 579 58.7 578 775 234 | Waiting: 52 579 58.7 578 775 235 | Total: 55 579 58.7 578 777 236 | 237 | Percentage of the requests served within a certain time (ms) 238 | 50% 578 239 | 66% 597 240 | 75% 610 241 | 80% 619 242 | 90% 641 243 | 95% 672 244 | 98% 703 245 | 99% 753 246 | 100% 777 (longest request) 247 | 248 | 249 | Time: 00:06.036, Memory: 16.00 MB 250 | 251 | OK (1 test, 1 assertion) 252 | ``` 253 | 254 | **压测创建一个完整的订单** 255 | 256 | ```shell 257 | root@DESKTOP-VQOELJ5:/web/linux/php/hyperf/hyperf22# composer test -- --testsuite=Transaction --filter=testBatchCreate05 258 | 259 | Completed 100 requests 260 | Completed 200 requests 261 | Completed 300 requests 262 | Completed 400 requests 263 | Completed 500 requests 264 | Completed 600 requests 265 | Completed 700 requests 266 | Completed 800 requests 267 | Completed 900 requests 268 | Completed 1000 requests 269 | Finished 1000 requests 270 | . 1 / 1 (100%) 271 | This is ApacheBench, Version 2.3 <$Revision: 1843412 $> 272 | Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ 273 | Licensed to The Apache Software Foundation, http://www.apache.org/ 274 | 275 | Benchmarking 127.0.0.1 (be patient) 276 | 277 | 278 | Server Software: Hyperf 279 | Server Hostname: 127.0.0.1 280 | Server Port: 9501 281 | 282 | Document Path: /api/resetAccountTest/createOrderWithLocal 283 | Document Length: 24 bytes 284 | 285 | Concurrency Level: 100 286 | Time taken for tests: 9.143 seconds 287 | Complete requests: 1000 288 | Failed requests: 0 289 | Total transferred: 167000 bytes 290 | Total body sent: 169000 291 | HTML transferred: 24000 bytes 292 | Requests per second: 109.37 [#/sec] (mean) 293 | Time per request: 914.296 [ms] (mean) 294 | Time per request: 9.143 [ms] (mean, across all concurrent requests) 295 | Transfer rate: 17.84 [Kbytes/sec] received 296 | 18.05 kb/s sent 297 | 35.89 kb/s total 298 | 299 | Connection Times (ms) 300 | min mean[+/-sd] median max 301 | Connect: 0 0 0.5 0 2 302 | Processing: 62 881 120.0 879 1448 303 | Waiting: 60 881 120.0 879 1448 304 | Total: 62 881 120.1 879 1449 305 | 306 | Percentage of the requests served within a certain time (ms) 307 | 50% 879 308 | 66% 897 309 | 75% 912 310 | 80% 918 311 | 90% 942 312 | 95% 1024 313 | 98% 1283 314 | 99% 1375 315 | 100% 1449 (longest request) 316 | 317 | 318 | Time: 00:09.205, Memory: 16.00 MB 319 | 320 | OK (1 test, 3 assertions) 321 | ``` 322 | 323 | **压测开启RT模式创建一个完整的订单** 324 | ``` 325 | root@DESKTOP-VQOELJ5:/web/linux/php/hyperf/hyperf22# composer test -- --testsuite=Transaction --filter=testBatchCreate06 326 | 327 | Completed 100 requests 328 | Completed 200 requests 329 | Completed 300 requests 330 | Completed 400 requests 331 | Completed 500 requests 332 | Completed 600 requests 333 | Completed 700 requests 334 | Completed 800 requests 335 | Completed 900 requests 336 | Completed 1000 requests 337 | Finished 1000 requests 338 | . 1 / 1 (100%) 339 | This is ApacheBench, Version 2.3 <$Revision: 1843412 $> 340 | Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ 341 | Licensed to The Apache Software Foundation, http://www.apache.org/ 342 | 343 | Benchmarking 127.0.0.1 (be patient) 344 | 345 | 346 | Server Software: Hyperf 347 | Server Hostname: 127.0.0.1 348 | Server Port: 9501 349 | 350 | Document Path: /api/resetAccountTest/createOrderWithRt 351 | Document Length: 24 bytes 352 | 353 | Concurrency Level: 100 354 | Time taken for tests: 18.640 seconds 355 | Complete requests: 1000 356 | Failed requests: 0 357 | Total transferred: 167000 bytes 358 | Total body sent: 166000 359 | HTML transferred: 24000 bytes 360 | Requests per second: 53.65 [#/sec] (mean) 361 | Time per request: 1863.969 [ms] (mean) 362 | Time per request: 18.640 [ms] (mean, across all concurrent requests) 363 | Transfer rate: 8.75 [Kbytes/sec] received 364 | 8.70 kb/s sent 365 | 17.45 kb/s total 366 | 367 | Connection Times (ms) 368 | min mean[+/-sd] median max 369 | Connect: 0 0 0.5 0 2 370 | Processing: 72 1823 245.0 1816 3075 371 | Waiting: 69 1823 245.0 1816 3075 372 | Total: 72 1823 245.2 1816 3076 373 | 374 | Percentage of the requests served within a certain time (ms) 375 | 50% 1816 376 | 66% 1837 377 | 75% 1850 378 | 80% 1860 379 | 90% 1917 380 | 95% 2082 381 | 98% 2699 382 | 99% 2897 383 | 100% 3076 (longest request) 384 | 385 | 386 | Time: 00:18.695, Memory: 16.00 MB 387 | 388 | OK (1 test, 3 assertions) 389 | ``` 390 | 391 | ## 参考教程 392 | https://learnku.com/articles/62377 393 | 394 | 395 | ![](https://cdn.learnku.com/uploads/images/202202/25/46914/heg3sLvwiG.jpg!large) 396 | 397 | 扫码进微信群。希望有更多的朋友相互学习和一起研究分布式事务的知识。 398 | 399 | -------------------------------------------------------------------------------- /src/Facades/ResetTransaction.php: -------------------------------------------------------------------------------- 1 | transactIdArr, $transactId); 25 | if (count($this->transactIdArr) == 1) { 26 | $data = [ 27 | 'transact_id' => $transactId, 28 | 'transact_rollback' => '[]', 29 | ]; 30 | Db::connection('rt_center')->table('reset_transact')->insert($data); 31 | } 32 | 33 | $this->stmtBegin(); 34 | 35 | return $this->getTransactId(); 36 | } 37 | 38 | public function commit() 39 | { 40 | $this->stmtRollback(); 41 | 42 | if (count($this->transactIdArr) > 1) { 43 | array_pop($this->transactIdArr); 44 | 45 | return true; 46 | } 47 | 48 | $this->logRT(RT::STATUS_COMMIT); 49 | 50 | $commitUrl = config('rt_database.center.commit_url'); 51 | 52 | $client = new Client([ 53 | 'handler' => HandlerStack::create(new CoroutineHandler()), 54 | 'timeout' => 120, 55 | 'swoole' => [ 56 | 'timeout' => 120, 57 | 'socket_buffer_size' => 1024 * 1024 * 2, 58 | ], 59 | ]); 60 | $response = $client->post($commitUrl, [ 61 | 'json' =>[ 62 | 'transact_id' => $this->getTransactId(), 63 | 'transact_rollback' => $this->transactRollback, 64 | ] 65 | ]); 66 | // $this->centerCommit($this->getTransactId(), $this->transactRollback); 67 | // $response = null; 68 | 69 | $this->removeRT(); 70 | 71 | return $response; 72 | } 73 | 74 | public function rollBack() 75 | { 76 | $this->stmtRollback(); 77 | 78 | if (count($this->transactIdArr) > 1) { 79 | $transactId = $this->getTransactId(); 80 | foreach ($this->transactRollback as $i => $txId) { 81 | if (strpos($txId, $transactId) === 0) { 82 | unset($this->transactRollback[$i]); 83 | } 84 | } 85 | array_push($this->transactRollback, $transactId); 86 | array_pop($this->transactIdArr); 87 | return true; 88 | } 89 | 90 | $this->logRT(RT::STATUS_ROLLBACK); 91 | 92 | $rollbackUrl = config('rt_database.center.rollback_url'); 93 | 94 | $client = new Client(); 95 | $response = $client->post($rollbackUrl, [ 96 | 'json' =>[ 97 | 'transact_id' => $this->getTransactId(), 98 | 'transact_rollback' => $this->transactRollback, 99 | ] 100 | ]); 101 | $this->removeRT(); 102 | 103 | return $response; 104 | } 105 | 106 | 107 | public function centerCommit($transactId, $transactRollback) 108 | { 109 | $item = Db::connection('rt_center')->table('reset_transact')->where('transact_id', $transactId)->first(); 110 | if ($item) { 111 | if ($item->transact_rollback) { 112 | $rollArr = json_decode($item->transact_rollback, true); 113 | $transactRollback = array_merge($transactRollback, $rollArr); 114 | } 115 | foreach ($transactRollback as $tid) { 116 | Db::connection('rt_center')->table('reset_transact_sql')->where('transact_id', 'like', $tid . '%')->update(['transact_status' => RT::STATUS_ROLLBACK]); 117 | } 118 | $xidMap = $this->getUsedXidMap($transactId, 'commit'); 119 | $xidArr = []; 120 | foreach ($xidMap as $name => $item) { 121 | $xidArr[$name] = $item['xid']; 122 | } 123 | 124 | $this->xaBeginTransaction($xidArr); 125 | $count = count($xidMap); 126 | $parallel = new Parallel($count); 127 | foreach ($xidMap as $name => $item) { 128 | $parallel->add(function() use($name, $item){ 129 | $sqlCollects = $item['sql_list']; 130 | foreach ($sqlCollects as $item) { 131 | $result = Db::connection($name)->getPdo()->exec($item->sql); 132 | if ($item->check_result && $result != $item->result) { 133 | throw new ResetTransactionException("db had been changed by anothor transact_id"); 134 | } 135 | } 136 | }); 137 | } 138 | 139 | try { 140 | $parallel->wait(); 141 | $this->xaCommit($xidArr); 142 | } catch(ParallelExecutionException $ex) { 143 | $this->xaRollBack($xidArr); 144 | 145 | throw $ex; 146 | } 147 | 148 | // Db::connection('rt_center')->table('reset_transact_sql')->where('transact_id', 'like', $transactId . '%')->delete(); 149 | // Db::connection('rt_center')->table('reset_transact_req')->where('transact_id', $transactId)->delete(); 150 | // Db::connection('rt_center')->table('reset_transact')->where('transact_id', $transactId)->delete(); 151 | } 152 | 153 | } 154 | 155 | public function centerRollback($transactId, $transactRollback) 156 | { 157 | if (strpos('-', $transactId)) { 158 | foreach ($transactRollback as $txId) { 159 | Db::connection('rt_center')->table('reset_transact_sql')->where('transact_id', 'like', $txId . '%')->update(['transact_status' => RT::STATUS_ROLLBACK]); 160 | } 161 | Db::connection('rt_center')->table('reset_transact_sql')->where('transact_id', 'like', $transactId . '%')->update(['transact_status' => RT::STATUS_ROLLBACK]); 162 | } else { 163 | Db::connection('rt_center')->table('reset_transact_sql')->where('transact_id', 'like', $transactId . '%')->delete(); 164 | Db::connection('rt_center')->table('reset_transact_req')->where('transact_id', $transactId)->delete(); 165 | Db::connection('rt_center')->table('reset_transact')->where('transact_id', $transactId)->delete(); 166 | } 167 | 168 | $this->removeRT(); 169 | } 170 | 171 | public function middlewareBeginTransaction($transactId) 172 | { 173 | // $resolver = ApplicationContext::getContainer()->get(ConnectionResolverInterface::class); 174 | // $connName = $resolver->getDefaultConnection(); 175 | $connName = Context::get('rt_connection_defaultName'); 176 | $transactIdArr = explode('-', $transactId); 177 | $sqlArr = Db::connection('rt_center') 178 | ->table('reset_transact_sql') 179 | ->where('transact_id', 'like', $transactIdArr[0].'%') 180 | ->where('connection', $connName) 181 | ->whereIn('transact_status', [RT::STATUS_START, RT::STATUS_COMMIT]) 182 | ->pluck('sql')->toArray(); 183 | $sql = implode(';', $sqlArr); 184 | $this->stmtBegin(); 185 | if ($sqlArr) { 186 | Db::unprepared($sql); 187 | } 188 | 189 | $this->setTransactId($transactId); 190 | } 191 | 192 | public function middlewareRollback() 193 | { 194 | $this->stmtRollback(); 195 | $this->logRT(RT::STATUS_COMMIT); 196 | 197 | if ($this->transactRollback) { 198 | $transactId = RT::getTransactId(); 199 | $transactIdArr = explode('-', $transactId); 200 | $tid = $transactIdArr[0]; 201 | 202 | $item = Db::connection('rt_center')->table('reset_transact')->where('transact_id', $tid)->first(); 203 | $arr = $item->transact_rollback ? json_decode($item->transact_rollback, true) : []; 204 | $arr = array_merge($arr, $this->transactRollback); 205 | $arr = array_unique($arr); 206 | 207 | $data = ['transact_rollback' => json_encode($arr)]; 208 | Db::connection('rt_center')->table('reset_transact')->where('transact_id', $tid)->update($data); 209 | } 210 | 211 | $this->removeRT(); 212 | } 213 | 214 | public function commitTest() 215 | { 216 | $this->stmtRollback(); 217 | 218 | if (count($this->transactIdArr) > 1) { 219 | array_pop($this->transactIdArr); 220 | 221 | return true; 222 | } 223 | 224 | $this->logRT(RT::STATUS_COMMIT); 225 | } 226 | 227 | public function rollBackTest() 228 | { 229 | $this->stmtRollback(); 230 | 231 | if (count($this->transactIdArr) > 1) { 232 | $transactId = $this->getTransactId(); 233 | foreach ($this->transactRollback as $i => $txId) { 234 | if (strpos($txId, $transactId) === 0) { 235 | unset($this->transactRollback[$i]); 236 | } 237 | } 238 | array_push($this->transactRollback, $transactId); 239 | array_pop($this->transactIdArr); 240 | return true; 241 | } 242 | 243 | $this->logRT(RT::STATUS_ROLLBACK); 244 | } 245 | 246 | public function setTransactId($transactId) 247 | { 248 | $this->transactIdArr = explode('-', $transactId); 249 | } 250 | 251 | 252 | public function getTransactId() 253 | { 254 | return implode('-', $this->transactIdArr); 255 | } 256 | 257 | public function getTransactRollback() 258 | { 259 | return $this->transactRollback; 260 | } 261 | 262 | private function getUsedXidMap($transactId, $action) 263 | { 264 | $xidMap = []; 265 | $query = Db::connection('rt_center')->table('reset_transact_sql')->where('transact_id', 'like', $transactId . '%'); 266 | if ($action == 'commit') { 267 | $query->whereIn('transact_status', [RT::STATUS_START, RT::STATUS_COMMIT]); 268 | } 269 | $list = $query->get(); 270 | foreach ($list as $item) { 271 | $name = $item->connection; 272 | $xidMap[$name]['sql_list'][] = $item; 273 | } 274 | 275 | foreach ($xidMap as $name => &$item){ 276 | $xid = session_create_id(); 277 | $item['xid'] = $xid; 278 | } 279 | 280 | return $xidMap; 281 | } 282 | 283 | public function logRT($status) 284 | { 285 | $sqlArr = Context::get('rt_transact_sql'); 286 | $requestId = Context::get('rt_request_id'); 287 | if (is_null($requestId) && $this->transactIdArr) { 288 | $requestId = $this->transactIdArr[0]; 289 | } 290 | 291 | if ($sqlArr) { 292 | foreach ($sqlArr as $item) { 293 | Db::connection('rt_center')->table('reset_transact_sql')->insert([ 294 | 'request_id' => $requestId, 295 | 'transact_id' => $item['transact_id'], 296 | 'transact_status' => $status, 297 | 'sql' => value($item['sql']), 298 | 'result' => $item['result'], 299 | 'check_result' => $item['check_result'], 300 | 'connection' => $item['connection'], 301 | ]); 302 | } 303 | } 304 | } 305 | 306 | private function removeRT() 307 | { 308 | $this->transactIdArr = []; 309 | 310 | Context::destroy('rt_transact_sql'); 311 | Context::destroy('rt_request_id'); 312 | } 313 | 314 | 315 | /** 316 | * beginTransaction 317 | * 318 | */ 319 | public function xaBeginTransaction($xidArr) 320 | { 321 | // foreach ($xidArr as $name => $xid) { 322 | // Db::connection($name)->beginTransaction(); 323 | // } 324 | $this->_XAStart($xidArr); 325 | } 326 | 327 | /** 328 | * commit 329 | * @param $xidArr 330 | */ 331 | public function xaCommit($xidArr) 332 | { 333 | // foreach ($xidArr as $name => $xid) { 334 | // Db::connection($name)->commit(); 335 | // } 336 | $this->_XAEnd($xidArr); 337 | $this->_XAPrepare($xidArr); 338 | $this->_XACommit($xidArr); 339 | } 340 | 341 | /** 342 | * rollback 343 | * @param $xidArr 344 | */ 345 | public function xaRollBack($xidArr) 346 | { 347 | $this->_XAEnd($xidArr); 348 | $this->_XAPrepare($xidArr); 349 | $this->_XARollback($xidArr); 350 | } 351 | 352 | private function _XAStart($xidArr) 353 | { 354 | foreach ($xidArr as $name => $xid) { 355 | Db::connection($name)->getPdo()->exec("XA START '{$xid}'"); 356 | } 357 | } 358 | 359 | 360 | private function _XAEnd($xidArr) 361 | { 362 | foreach ($xidArr as $name => $xid) { 363 | Db::connection($name)->getPdo()->exec("XA END '{$xid}'"); 364 | } 365 | } 366 | 367 | 368 | private function _XAPrepare($xidArr) 369 | { 370 | foreach ($xidArr as $name => $xid) { 371 | Db::connection($name)->getPdo()->exec("XA PREPARE '{$xid}'"); 372 | } 373 | } 374 | 375 | 376 | private function _XACommit($xidArr) 377 | { 378 | foreach ($xidArr as $name => $xid) { 379 | Db::connection($name)->getPdo()->exec("XA COMMIT '{$xid}'"); 380 | } 381 | } 382 | 383 | private function _XARollback($xidArr) 384 | { 385 | foreach ($xidArr as $name => $xid) { 386 | Db::connection($name)->getPdo()->exec("XA ROLLBACK '{$xid}'"); 387 | } 388 | } 389 | 390 | public function saveQuery($query, $bindings, $result, $checkResult, $keyName = null, $id = null) 391 | { 392 | $rtTransactId = $this->getTransactId(); 393 | if ($rtTransactId && $query && !strpos($query, 'reset_transact')) { 394 | $subString = strtolower(substr(trim($query), 0, 12)); 395 | $actionArr = explode(' ', $subString); 396 | $action = $actionArr[0]; 397 | 398 | $sql = str_replace("?", "'%s'", $query); 399 | $completeSql = vsprintf($sql, $bindings); 400 | 401 | $connection = Context::get('rt_connection'); 402 | $conName = $connection->getConfig('connection_name'); 403 | if (is_null($conName)) { 404 | throw new ResetTransactionException('rt database config [connection_name] can not be null'); 405 | } 406 | 407 | if (in_array($action, ['insert', 'update', 'delete', 'set', 'savepoint', 'rollback'])) { 408 | $backupSql = $completeSql; 409 | if ($action == 'insert') { 410 | // if only queryBuilder insert or batch insert then return false 411 | if (is_null($id)) { 412 | return false; 413 | } 414 | 415 | if (is_null($keyName)) { 416 | $keyName = 'id'; 417 | } 418 | 419 | if (!strpos($query, "`{$keyName}`")) { 420 | // extract variables from sql 421 | preg_match("/insert into (.+) \((.+)\) values \((.+)\)/", $backupSql, $match); 422 | $table = $match[1]; 423 | $columns = $match[2]; 424 | $parameters = $match[3]; 425 | 426 | $columns = "`{$keyName}`, " . $columns; 427 | $parameters = "'{$id}', " . $parameters; 428 | $backupSql = "insert into $table ($columns) values ($parameters)"; 429 | } 430 | } 431 | 432 | $sqlItem = [ 433 | 'transact_id' => $rtTransactId, 434 | 'sql' => $backupSql, 435 | 'result' => $result, 436 | 'check_result' => (int) $checkResult, 437 | 'connection' => $conName, 438 | ]; 439 | Context::override('rt_transact_sql', function ($value) use ($sqlItem) { 440 | if (is_null($value)) { 441 | $value = []; 442 | } 443 | $value[] = $sqlItem; 444 | 445 | return $value; 446 | }); 447 | } 448 | 449 | } 450 | } 451 | 452 | private function stmtBegin() 453 | { 454 | Context::set('rt_stmt', 'begin'); 455 | Db::beginTransaction(); 456 | Context::destroy('rt_stmt'); 457 | } 458 | 459 | private function stmtRollback() 460 | { 461 | Context::set('rt_stmt', 'rollback'); 462 | Db::rollBack(); 463 | Context::destroy('rt_stmt'); 464 | } 465 | } 466 | --------------------------------------------------------------------------------