分散式事務(引入)
面試題
你簡歷上寫用微服務boot/cloud做過專案,你不可能只有一個資料庫吧?請你談談多個資料庫之間,你如何處理分散式事務?
舉例:在訂單支付成功後,交易中心會呼叫訂單中心的服務把訂單狀態更新,並呼叫物流中心的服務通知商品發貨,同時還要呼叫積分中心的服務為使用者增加相應的積分。如何保障分散式事務一致性,成為了確保訂單業務穩定執行的核心訴求之一。
阿里巴巴的Seata-AT模式如何做到對業務的無侵入?
對於分散式事務,你知道的解決方案有那些?請你談談?
- 2PC(兩階段提交)
- 3PC(三階段提交)
- TCC方案[TCC(Try-Confirm-Cancel)被稱為補償事務),類似2PC的柔性分散式解決方案,2PC改良版]
- LocalMessage本地訊息表
- 獨立訊息微服務 + RabbitMQ/Kafka元件,實現可靠訊息最終一致性方案
- 最大努力通知方案...
分散式事務問題,如何產生?先看業務
上述面試問題都指向一個重要問題?
一次業務操作需要跨多個資料來源或需要跨多個系統(多個程序)進行遠端呼叫,就會產生分散式事務問題,但是關係型資料庫提供的能力是基於單機事務的,一旦遇到分散式事務場景,就需要透過更多其他技術手段來解決問題。
微服務出現之前
單機單庫沒有分散式事務的問題。
微服務出現之後
單體應用被拆分成微服務應用,原來的三個模組,被拆分成三個對立的應用,分別使用三個獨立的資料來源,業務操作需要呼叫三個服務來完成,此時每個服務自己內部的資料一致性由本地事務來保證,但是全域性的資料一致性問題無法保證。
結論
迫切希望提供一種分散式事務框架,解決微服務架構下的分散式事務問題。
1.Seata簡介
1.1 是什麼
Simple Extensible Autonomous Transaction Architecture(簡單可擴充套件自治事務框架) -- Seata
1.1.1 官網解釋
Apache Seata(incubating) 是一款開源的分散式事務解決方案,致力於在微服務架構下提供高效能和簡單易用的分散式事務服務。
阿里,已經將其捐獻給了Apache基金會。
1.1.2 發展歷程
阿里巴巴作為國內最早一批進行應用分散式(微服務化)改造的企業,很早就遇到微服務架構下的分散式事務問題。
2019年1月份螞蟻金服和阿里巴巴共同開源的分散式事務解決方案:
2014 年,阿里中介軟體團隊釋出 TXC(Taobao Transaction Constructor),為集團內應用提供分散式事務服務。
2016 年,TXC 在經過產品化改造後,以 GTS(Global Transaction Service) 的身份登陸阿里雲,成為當時業界唯一一款雲上分散式事務產品。在阿雲裡的公有云、專有云解決方案中,開始服務於眾多外部客戶。
2019 年起,基於 TXC 和 GTS 的技術積累,阿里中介軟體團隊發起了開源專案 Fescar(Fast & EaSy Commit And Rollback, FESCAR),和社群一起建設這個分散式事務解決方案。
2019 年 fescar(全稱fast easy commit and rollback) 被重新命名為了seata(simple extensiable autonomous transaction architecture)。TXC、GTS、Fescar 以及 seata 一脈相承,為解決微服務架構下的分散式事務問題交出了一份與眾不同的答卷。
1.2 功能
Seata
是一款開源的分散式事務解決方案,致力於在微服務架構下提供高效能和簡單易用的分散式事務服務。
1.3 下載地址
官網
GitHub
1.4 怎麼用
Spring中的本地事務@Transactional
,全域性事務@GlobalTransactional
Seata的分散式事務解決方案
2.Seata工作流程簡介(原理)
2.1 分散式事務的管理,就是全域性事務id的傳遞和變更,要讓開發者無感知
2.2 Seata對分散式事務的協調和控制就是1 + 3
2.2.1 1個XID
XID是全域性事務的唯一標識,它可以在服務的呼叫鏈路中傳遞,繫結到服務的事務上下文中。
2.2.2 官網版3個概念
2.2.3 周陽老師對Seata術語的解釋
TC(Tansaction Coordinator):事務協調器
就是Seata元件,負責維護全域性事務和分支事務的狀態,驅動
全域性事務提交或回滾。
TM(Transaction Manager):事務管理器
標註全域性@GlobalTransactional
啟動入口的微服務模組(介面),它是全域性事務的發起者,負責定義全域性事務的範圍,並根據TC維護的全域性事務和分支事務狀態,做出開始事務、提交事務、回滾事務的決議。
RM(Resource Manager):資源管理器
就是關係型資料庫本身,可以時多個RM,負責管理分支事務上的資源,向TC註冊分支事務,彙報分支事務狀態,驅動分支事務的提交或回滾。
注意,Seata的不同模式,使用的儲存,執行中產生的的全域性事務資料的介質不同,有的是檔案,有的是關係型資料庫的表,而且使用關係型資料庫作為儲存介質時,所支援的關係型資料庫也不同,具體見官網資料來源支援,這也是使用Seata作為分散式解決方案的限制之一。
2.3 分散式事務的執行流程-小總結
三個元件相互協作,TC以Seata 伺服器(Server)形式獨立部署,TM和RM則是以Seata Client的形式整合在微服務中執行,
A typical lifecycle of Seata managed distributed transaction:
- TM asks TC to begin a new global transaction. TC generates an XID representing the global transaction.
- XID is propagated through microservices' invoke chain.
- RM registers local transaction as a branch of the corresponding global transaction of XID to TC.
- TM asks TC for committing or rollbacking the corresponding global transaction of XID.
- TC drives all branch transactions under the corresponding global transaction of XID to finish branch committing or rollbacking.
1.TM 向 TC 申請開啟一個全域性事務,全域性事務建立成功並生成一個全域性唯一的 XID;
2.XID 在微服務呼叫鏈路的上下文中傳播;
3.RM 向 TC 註冊分支事務,將其納入 XID 對應全域性事務的管轄;
4.TM 向 TC 發起針對 XID 的全域性提交或回滾決議;
5.TC 排程XID 下管轄的全部分支事務完成提交或回滾請求。
2.4 Seata的事務模式
AT模式
周陽老師,本次課程,只介紹了AT模式。理由是,日常工作,企業調研和本次課時安排限制。
3.Seata-Server2.0.0安裝
3.1 下載
見本文1.3 下載地址
3.2 Seata引數官網參考
https://seata.apache.org/zh-cn/docs/user/configurations
3.3 Seata新手部署指南
Seata分TC,TM和RM三個角色,TC(Server)端為單獨服務端部署,TM和RM(Client端)由業務系統整合。
官網新手部署指南
3.4 MySQL8.0資料庫建庫 + 建表
3.4.1 建立TC使用的庫
create database seata;
use seata;
3.4.2 在上一步seata庫裡建表
建表sql地址
--
-- Licensed to the Apache Software Foundation (ASF) under one or more
-- contributor license agreements. See the NOTICE file distributed with
-- this work for additional information regarding copyright ownership.
-- The ASF licenses this file to You under the Apache License, Version 2.0
-- (the "License"); you may not use this file except in compliance with
-- the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- the table to store GlobalSession data
CREATE TABLE IF NOT EXISTS `global_table`
(
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`status` TINYINT NOT NULL,
`application_id` VARCHAR(32),
`transaction_service_group` VARCHAR(32),
`transaction_name` VARCHAR(128),
`timeout` INT,
`begin_time` BIGINT,
`application_data` VARCHAR(2000),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`xid`),
KEY `idx_status_gmt_modified` (`status` , `gmt_modified`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
`branch_id` BIGINT NOT NULL,
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`resource_group_id` VARCHAR(32),
`resource_id` VARCHAR(256),
`branch_type` VARCHAR(8),
`status` TINYINT,
`client_id` VARCHAR(64),
`application_data` VARCHAR(2000),
`gmt_create` DATETIME(6),
`gmt_modified` DATETIME(6),
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(128),
`transaction_id` BIGINT,
`branch_id` BIGINT NOT NULL,
`resource_id` VARCHAR(256),
`table_name` VARCHAR(32),
`pk` VARCHAR(36),
`status` TINYINT NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_status` (`status`),
KEY `idx_branch_id` (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
CREATE TABLE IF NOT EXISTS `distributed_lock`
(
`lock_key` CHAR(20) NOT NULL,
`lock_value` VARCHAR(20) NOT NULL,
`expire` BIGINT,
primary key (`lock_key`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);
CREATE TABLE IF NOT EXISTS `vgroup_table`
(
`vGroup` VARCHAR(255),
`namespace` VARCHAR(255),
`cluster` VARCHAR(255),
UNIQUE KEY `idx_vgroup_namespace_cluster` (`vGroup`,`namespace`,`cluster`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
3.5 更改配置
修改seata-server-2.0.0\conf\application.yml配置檔案,記得先備份(防止需要回退到原有的配置)
# Copyright 1999-2019 Seata.io Group.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
server:
port: 7091
spring:
application:
name: seata-server
logging:
config: classpath:logback-spring.xml
file:
path: ${log.home:${user.home}/logs/seata}
extend:
logstash-appender:
destination: 127.0.0.1:4560
kafka-appender:
bootstrap-servers: 127.0.0.1:9092
topic: logback_to_logstash
console:
user:
username: seata
password: seata
seata:
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace:
group: SEATA_GROUP #後續自己在nacos裡面新建,不想新建SEATA_GROUP,就寫DEFAULT_GROUP
username: nacos
password: nacos
registry:
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
group: SEATA_GROUP #後續自己在nacos裡面新建,不想新建SEATA_GROUP,就寫DEFAULT_GROUP
namespace:
cluster: default
username: nacos
password: nacos
store:
mode: db
db:
datasource: druid
db-type: mysql
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
user: root
password: 123456
min-conn: 10
max-conn: 100
global-table: global_table
branch-table: branch_table
lock-table: lock_table
distributed-lock-table: distributed_lock
query-limit: 1000
max-wait: 5000
# server:
# service-port: 8091 #If not configured, the default is '${server.port} + 1000'
security:
secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
tokenValidityInMilliseconds: 1800000
ignore:
urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.jpeg,/**/*.ico,/api/v1/auth/login,/metadata/v1/**
3.6 啟動Nacos2.2.3埠號8848
startup.cmd -m standalone
命令執行成功後訪問http://localhost:8848/nacos
3.7 啟動seata-server-2.0.0
進入seata軟體目錄\bin,啟動略
進入seata前端介面,http://localhost:7091
進入Nacos,服務列表,會有seata-server服務。
4.Seata案例實戰-資料庫和表準備
訂單 + 庫存 + 賬戶3個業務資料庫MySQL準備。
以下演示都需要先啟動Nacos後啟動Seata,保證兩個都OK。
4.1 分散式事務本案例業務說明
這裡我們建立三個服務,一個訂單服務,一個庫存服務,一個賬戶服務。**
當使用者下單時,會在訂單服務中建立一個訂單,然後透過遠端呼叫庫存服務來扣減下單商品的庫存,
再透過遠端呼叫賬戶服務來扣減使用者賬戶裡面的餘額,
最後在訂單服務中修改訂單狀態為已完成。該操作跨越三個資料庫,有兩次遠端呼叫,很明顯會有分散式事務問題。
4.2 建立3個業務資料庫DATABASE
seata_order:儲存訂單的資料庫;
seata_storage:儲存庫存的資料庫;
seata_account:儲存賬戶資訊的資料庫;
CREATE DATABASE seata_order;
CREATE DATABASE seata_storage;
CREATE DATABASE seata_account;
4.3 按照上述3庫分別建立對應的undo_log回滾日誌表
因為AT模式,是需要將分散式事務中,修改前後的資料,做一個快照,儲存到資料庫中,如果全域性事務失敗,分支(本地)事務需要回滾,方便修改本地事務已經提交的資料。所以,需要在單個微服務對應的資料庫中,建立回滾日誌表。成功的話,只要把記錄的這條資料刪除即可。全域性事務成功或失敗,沒必要保留此事務中的臨時資料。
4.3.1 undo_log表
https://github.com/apache/incubator-seata/blob/2.x/script/client/at/db/mysql.sql
CREATE TABLE IF NOT EXISTS `undo_log`
(
`branch_id` BIGINT NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id',
`context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info',
`log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` DATETIME(6) NOT NULL COMMENT 'create datetime',
`log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime',
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
ALTER TABLE `undo_log` ADD INDEX `ix_log_created` (`log_created`);
注意,在當前筆記中,是以AT模式的mysql資料來源,所以選擇的建表語句都是mysql版本的。不同資料來源見GitHub或Seata官網。
4.4 按照上述庫分別建立對應業務表和undo_log表
#訂單庫
CREATE DATABASE seata_order;
USE seata_order;
CREATE TABLE t_order(
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`user_id` BIGINT(11) DEFAULT NULL COMMENT '使用者id',
`product_id` BIGINT(11)DEFAULT NULL COMMENT '產品id',
`count` INT(11) DEFAULT NULL COMMENT '數量',
`money` DECIMAL(11,0) DEFAULT NULL COMMENT '金額',
`status` INT(1) DEFAULT NULL COMMENT '訂單狀態: 0:建立中; 1:已完結'
)ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
-- for AT mode you must to init this sql for you business database. the seata server not need it.
CREATE TABLE IF NOT EXISTS `undo_log`
(
`branch_id` BIGINT NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id',
`context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info',
`log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` DATETIME(6) NOT NULL COMMENT 'create datetime',
`log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime',
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
ALTER TABLE `undo_log` ADD INDEX `ix_log_created` (`log_created`);
#賬號庫
CREATE DATABASE seata_account;
USE seata_account;
CREATE TABLE t_account(
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'id',
`user_id` BIGINT(11) DEFAULT NULL COMMENT '使用者id',
`total` DECIMAL(10,0) DEFAULT NULL COMMENT '總額度',
`used` DECIMAL(10,0) DEFAULT NULL COMMENT '已用賬戶餘額',
`residue` DECIMAL(10,0) DEFAULT '0' COMMENT '剩餘可用額度'
)ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
#插入一條假資料
INSERT INTO t_account(`id`,`user_id`,`total`,`used`,`residue`)VALUES('1','1','1000','0','1000');
-- for AT mode you must to init this sql for you business database. the seata server not need it.
CREATE TABLE IF NOT EXISTS `undo_log`
(
`branch_id` BIGINT NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id',
`context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info',
`log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` DATETIME(6) NOT NULL COMMENT 'create datetime',
`log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime',
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
ALTER TABLE `undo_log` ADD INDEX `ix_log_created` (`log_created`);
#庫存庫
CREATE DATABASE seata_storage;
USE seata_storage;
CREATE TABLE t_storage(
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`product_id` BIGINT(11) DEFAULT NULL COMMENT '產品id',
`total` INT(11) DEFAULT NULL COMMENT '總庫存',
`used` INT(11) DEFAULT NULL COMMENT '已用庫存',
`residue` INT(11) DEFAULT NULL COMMENT '剩餘庫存'
)ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
#插入一條假資料
INSERT INTO t_storage(`id`,`product_id`,`total`,`used`,`residue`)VALUES('1','1','100','0','100');
-- for AT mode you must to init this sql for you business database. the seata server not need it.
CREATE TABLE IF NOT EXISTS `undo_log`
(
`branch_id` BIGINT NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id',
`context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info',
`log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` DATETIME(6) NOT NULL COMMENT 'create datetime',
`log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime',
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
ALTER TABLE `undo_log` ADD INDEX `ix_log_created` (`log_created`);
4.5 最終效果
在MySQL8.0中有seata庫,seata_order庫,seata_storage庫,seata_account庫,其中有對應的表,表資訊略,見上sql語句。
5.Seata案例實戰-微服務編碼落地實現
案例需求:下訂單 -> 減庫存 -> 扣餘額 -> 改(訂單)狀態
5.1 使用外掛生成基礎類
使用最開始的generator模組,和之前生成t_pay表的基礎類相同。
5.1.1 config.properties
#t_pay表包名
package.name=com.atguigu.cloud
# mysql8.0
jdbc.driverClass = com.mysql.cj.jdbc.Driver
jdbc.url= jdbc:mysql://localhost:3306/db2024?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
jdbc.user = root
jdbc.password =123456
# seata_order
#jdbc.driverClass = com.mysql.cj.jdbc.Driver
#jdbc.url = jdbc:mysql://localhost:3306/seata_order?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
#jdbc.user = root
#jdbc.password =123456
# seata_storage
#jdbc.driverClass = com.mysql.cj.jdbc.Driver
#jdbc.url = jdbc:mysql://localhost:3306/seata_storage?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
#jdbc.user = root
#jdbc.password =123456
# seata_account
#jdbc.driverClass = com.mysql.cj.jdbc.Driver
#jdbc.url = jdbc:mysql://localhost:3306/seata_account?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
#jdbc.user = root
#jdbc.password =123456
5.1.2 generatorConfig.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<properties resource="config.properties"/>
<context id="Mysql" targetRuntime="MyBatis3Simple" defaultModelType="flat">
<property name="beginningDelimiter" value="`"/>
<property name="endingDelimiter" value="`"/>
<plugin type="tk.mybatis.mapper.generator.MapperPlugin">
<property name="mappers" value="tk.mybatis.mapper.common.Mapper"/>
<property name="caseSensitive" value="true"/>
</plugin>
<jdbcConnection driverClass="${jdbc.driverClass}"
connectionURL="${jdbc.url}"
userId="${jdbc.user}"
password="${jdbc.password}">
</jdbcConnection>
<javaModelGenerator targetPackage="${package.name}.entities" targetProject="src/main/java"/>
<sqlMapGenerator targetPackage="${package.name}.mapper" targetProject="src/main/java"/>
<javaClientGenerator targetPackage="${package.name}.mapper" targetProject="src/main/java" type="XMLMAPPER"/>
<table tableName="t_pay" domainObjectName="Pay">
<generatedKey column="id" sqlStatement="JDBC"/>
</table>
<!-- seata_order -->
<!--<table tableName="t_order" domainObjectName="Order">
<generatedKey column="id" sqlStatement="JDBC"/>
</table>-->
<!--seata_storage-->
<!--<table tableName="t_storage" domainObjectName="Storage">
<generatedKey column="id" sqlStatement="JDBC"/>
</table>-->
<!--seata_account-->
<!--<table tableName="t_account" domainObjectName="Account">
<generatedKey column="id" sqlStatement="JDBC"/>
</table>-->
</context>
</generatorConfiguration>
5.1.3 生成
注意,jdbc連線時,修改為自己建立的庫和賬號密碼,生成略。
5.2 修改公共cloud-api-commons新增庫存和賬號兩個Feign服務介面
package com.atguigu.cloud.apis;
import com.atguigu.cloud.resp.ResultData;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(value = "seata-storage-service")
public interface StorageFeignApi {
/**
* 扣減庫存
*/
@PostMapping(value = "/storage/decrease")
ResultData decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}
package com.atguigu.cloud.apis;
import com.atguigu.cloud.resp.ResultData;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(value = "seata-account-service")
public interface AccountFeignApi {
//扣減賬戶餘額
@PostMapping("/account/decrease")
ResultData decrease(@RequestParam("userId") Long userId, @RequestParam("money") Long money);
}
5.3 新建訂單Order微服務
5.3.1 建Module
seata-order-service2001
5.3.2 改POM
<dependencies>
<!-- nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--alibaba-seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--loadbalancer-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!--cloud-api-commons-->
<dependency>
<groupId>com.atguigu.cloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--web + actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--SpringBoot整合druid連線池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<!-- Swagger3 呼叫方式 http://你的主機IP地址:5555/swagger-ui/index.html -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
<!--mybatis和springboot整合-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!--Mysql資料庫驅動8 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--persistence-->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
</dependency>
<!--通用Mapper4-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!-- fastjson2 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
<scope>provided</scope>
</dependency>
<!--test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
5.3.3 寫YML
server:
port: 2001
spring:
application:
name: seata-order-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服務註冊中心地址
# ==========applicationName + druid-mysql8 driver===================
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_order?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
username: root
password: 123456
# ========================mybatis===================
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.atguigu.cloud.entities
configuration:
map-underscore-to-camel-case: true
# ========================seata===================
seata:
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: ""
group: SEATA_GROUP
application: seata-server
tx-service-group: default_tx_group # 事務組,由它獲得TC服務的叢集名稱
service:
vgroup-mapping: # 點選原始碼分析
default_tx_group: default # 事務組與TC服務叢集的對映關係
data-source-proxy-mode: AT
logging:
level:
io:
seata: info
Nacos,為什麼設計Namespace + Group + DataId三者關係?
Nacos資料模型Key由三元組唯一確定,Namespace預設是空串,公共名稱空間(public),分組預設是DEFAULT_GROUP。
上圖落地的對應原始碼(筆記最下面還有):io.seata.spring.boot.autoconfigure.properties.client.ServiceProperties
@Override
public void afterPropertiesSet() throws Exception {
if (0 == vgroupMapping.size()) {
vgroupMapping.put(DEFAULT_TX_GROUP, DEFAULT_TC_CLUSTER);
//compatible with old value, will remove next version
vgroupMapping.put(DEFAULT_TX_GROUP_OLD, DEFAULT_TC_CLUSTER);
}
if (0 == grouplist.size()) {
grouplist.put(DEFAULT_TC_CLUSTER, DEFAULT_GROUPLIST);
}
}
上圖落地的對應原始碼:io.seata.common.DefaultValues
String DEFAULT_TX_GROUP = "default_tx_group"; //預設事務分組
@Deprecated
String DEFAULT_TX_GROUP_OLD = "my_test_tx_group";//之前版本的預設事務分組,已過時
String DEFAULT_TC_CLUSTER = "default";//預設叢集對映關係
String DEFAULT_GROUPLIST = "127.0.0.1:8091";
String DEFAULT_DATA_SOURCE_PROXY_MODE = "AT";//預設模式
詳細過度版的配置
#seata:
# registry: # seata註冊配置
# type: nacos # seata註冊型別
# nacos:
# application: seata-server #seata應用名稱
# server-addr: 127.0.0.1:8848
# namespace: ""
# group: SEATA_GROUP
# cluster: default
# config: # seata配置抓取
# nacos:
# server-addr: 127.0.0.1:8848
# namespace: ""
# group: SEATA_GROUP
# username: nacos
# password: nacos
# tx-service-group: default_tx_group # 事務組,由它獲得TC服務的叢集名稱
# service:
# vgroup-mapping:
# default_tx_group: default # 事務群組的對映配置關係
# data-source-proxy-mode: AT
# application-id: seata-server
5.3.4 主啟動
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import tk.mybatis.spring.annotation.MapperScan;
@SpringBootApplication
@MapperScan("com.atguigu.cloud.mapper") //import tk.mybatis.spring.annotation.MapperScan;
@EnableDiscoveryClient //服務註冊和發現
@EnableFeignClients
public class SeataOrderMainApp2001 {
public static void main(String[] args) {
SpringApplication.run(SeataOrderMainApp2001.class, args);
}
}
5.3.5 業務類
entities
package com.atguigu.cloud.entities;
import lombok.ToString;
import javax.persistence.*;
import java.io.Serializable;
/**
* 表名:t_order
*/
@Table(name = "t_order")
@ToString
public class Order implements Serializable {
@Id
@GeneratedValue(generator = "JDBC")
private Long id;
/**
* 使用者id
*/
@Column(name = "user_id")
private Long userId;
/**
* 產品id
*/
@Column(name = "product_id")
private Long productId;
/**
* 數量
*/
private Integer count;
/**
* 金額
*/
private Long money;
/**
* 訂單狀態:0:建立中;1:已完結
*/
private Integer status;
/**
* @return id
*/
public Long getId() {
return id;
}
/**
* @param id
*/
public void setId(Long id) {
this.id = id;
}
/**
* 獲取使用者id
*
* @return userId - 使用者id
*/
public Long getUserId() {
return userId;
}
/**
* 設定使用者id
*
* @param userId 使用者id
*/
public void setUserId(Long userId) {
this.userId = userId;
}
/**
* 獲取產品id
*
* @return productId - 產品id
*/
public Long getProductId() {
return productId;
}
/**
* 設定產品id
*
* @param productId 產品id
*/
public void setProductId(Long productId) {
this.productId = productId;
}
/**
* 獲取數量
*
* @return count - 數量
*/
public Integer getCount() {
return count;
}
/**
* 設定數量
*
* @param count 數量
*/
public void setCount(Integer count) {
this.count = count;
}
/**
* 獲取金額
*
* @return money - 金額
*/
public Long getMoney() {
return money;
}
/**
* 設定金額
*
* @param money 金額
*/
public void setMoney(Long money) {
this.money = money;
}
/**
* 獲取訂單狀態:0:建立中;1:已完結
*
* @return status - 訂單狀態:0:建立中;1:已完結
*/
public Integer getStatus() {
return status;
}
/**
* 設定訂單狀態:0:建立中;1:已完結
*
* @param status 訂單狀態:0:建立中;1:已完結
*/
public void setStatus(Integer status) {
this.status = status;
}
}
OrderMapper
OrderMapper介面
import com.atguigu.cloud.entities.Order;
import tk.mybatis.mapper.common.Mapper;
public interface OrderMapper extends Mapper<Order> {
}
resources資料夾下新建mapper資料夾後新增OrderMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.cloud.mapper.OrderMapper">
<resultMap id="BaseResultMap" type="com.atguigu.cloud.entities.Order">
<!--
WARNING - @mbg.generated
-->
<id column="id" jdbcType="BIGINT" property="id" />
<result column="user_id" jdbcType="BIGINT" property="userId" />
<result column="product_id" jdbcType="BIGINT" property="productId" />
<result column="count" jdbcType="INTEGER" property="count" />
<result column="money" jdbcType="DECIMAL" property="money" />
<result column="status" jdbcType="INTEGER" property="status" />
</resultMap>
</mapper>
Service介面及實現
import com.atguigu.cloud.entities.Order;
public interface OrderService {
/**
* 建立訂單
*/
void create(Order order);
}
import com.atguigu.cloud.entities.Order;
import com.atguigu.cloud.service.OrderService;
public class OrderServiceImpl implements OrderService {
@Resource
private OrderMapper orderMapper;
@Resource//訂單微服務透過OpenFeign去呼叫庫存微服務
private StorageFeignApi storageFeignApi;
@Resource//訂單微服務透過OpenFeign去呼叫賬戶微服務
private AccountFeignApi accountFeignApi;
@Override
@GlobalTransactional(name = "zzyy-create-order",rollbackFor = Exception.class) //AT
public void create(Order order) {
//XID檢查
String xid = RootContext.getXID();//底層使用的ThreadLocal儲存xid資訊
//1. 新建訂單
log.info("==================>開始新建訂單"+"\t"+"xid_order:" +xid);
//訂單狀態status:0:建立中;1:已完結
order.setStatus(0);
int result = orderMapper.insertSelective(order);
//插入訂單成功後獲得插入mysql的實體物件
Order orderFromDB = null;
if(result > 0) {
orderFromDB = orderMapper.selectOne(order);
//orderFromDB = orderMapper.selectByPrimaryKey(order.getId());
log.info("-------> 新建訂單成功,orderFromDB info: "+orderFromDB);
System.out.println();
//2. 扣減庫存
log.info("-------> 訂單微服務開始呼叫Storage庫存,做扣減count");
storageFeignApi.decrease(orderFromDB.getProductId(), orderFromDB.getCount());
log.info("-------> 訂單微服務結束呼叫Storage庫存,做扣減完成");
System.out.println();
//3. 扣減賬號餘額
log.info("-------> 訂單微服務開始呼叫Account賬號,做扣減money");
accountFeignApi.decrease(orderFromDB.getUserId(), orderFromDB.getMoney());
log.info("-------> 訂單微服務結束呼叫Account賬號,做扣減完成");
System.out.println();
//4. 修改訂單狀態
//訂單狀態status:0:建立中;1:已完結
log.info("-------> 修改訂單狀態");
orderFromDB.setStatus(1);
Example whereCondition=new Example(Order.class);
Example.Criteria criteria=whereCondition.createCriteria();
criteria.andEqualTo("userId",orderFromDB.getUserId());
criteria.andEqualTo("status",0);
int updateResult = orderMapper.updateByExampleSelective(orderFromDB, whereCondition);
log.info("-------> 修改訂單狀態完成"+"\t"+updateResult);
log.info("-------> orderFromDB info: "+orderFromDB);
}
System.out.println();
log.info("==================>結束新建訂單"+"\t"+"xid_order:" +xid);
}
}
OrderController
import com.atguigu.cloud.entities.Order;
import com.atguigu.cloud.resp.ResultData;
import com.atguigu.cloud.service.OrderService;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class OrderController {
@Resource
private OrderService orderService;
/**
* 建立訂單
*/
@GetMapping("/order/create")
public ResultData create(Order order)
{
orderService.create(order);
return ResultData.success(order);
}
}
5.4 新建庫存Storage微服務
5.4.1 建Module
seata-storage-service2002
5.4.2 改POM
<dependencies>
<!-- nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--alibaba-seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--loadbalancer-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!--cloud_commons_utils-->
<dependency>
<groupId>com.atguigu.cloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--web + actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--SpringBoot整合druid連線池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<!-- Swagger3 呼叫方式 http://你的主機IP地址:5555/swagger-ui/index.html -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
<!--mybatis和springboot整合-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!--Mysql資料庫驅動8 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--persistence-->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
</dependency>
<!--通用Mapper4-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!-- fastjson2 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
<scope>provided</scope>
</dependency>
<!--test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
5.4.3 寫YML
server:
port: 2002
spring:
application:
name: seata-storage-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服務註冊中心地址
# ==========applicationName + druid-mysql8 driver===================
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_storage?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
username: root
password: 123456
# ========================mybatis===================
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.atguigu.cloud.entities
configuration:
map-underscore-to-camel-case: true
# ========================seata===================
seata:
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: ""
group: SEATA_GROUP
application: seata-server
tx-service-group: default_tx_group # 事務組,由它獲得TC服務的叢集名稱
service:
vgroup-mapping:
default_tx_group: default # 事務組與TC服務叢集的對映關係
data-source-proxy-mode: AT
logging:
level:
io:
seata: info
5.4.4 主啟動
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import tk.mybatis.spring.annotation.MapperScan;
@SpringBootApplication
@MapperScan("com.atguigu.cloud.mapper") //import tk.mybatis.spring.annotation.MapperScan;
@EnableDiscoveryClient //服務註冊和發現
@EnableFeignClients
public class SeataStorageMainApp2002 {
public static void main(String[] args) {
SpringApplication.run(SeataStorageMainApp2002.class, args);
}
}
5.4.5 業務類
entities
package com.atguigu.cloud.entities;
import javax.persistence.*;
/**
* 表名:t_storage
*/
@Table(name = "t_storage")
public class Storage {
@Id
@GeneratedValue(generator = "JDBC")
private Long id;
/**
* 產品id
*/
@Column(name = "product_id")
private Long productId;
/**
* 總庫存
*/
private Integer total;
/**
* 已用庫存
*/
private Integer used;
/**
* 剩餘庫存
*/
private Integer residue;
/**
* @return id
*/
public Long getId() {
return id;
}
/**
* @param id
*/
public void setId(Long id) {
this.id = id;
}
/**
* 獲取產品id
*
* @return productId - 產品id
*/
public Long getProductId() {
return productId;
}
/**
* 設定產品id
*
* @param productId 產品id
*/
public void setProductId(Long productId) {
this.productId = productId;
}
/**
* 獲取總庫存
*
* @return total - 總庫存
*/
public Integer getTotal() {
return total;
}
/**
* 設定總庫存
*
* @param total 總庫存
*/
public void setTotal(Integer total) {
this.total = total;
}
/**
* 獲取已用庫存
*
* @return used - 已用庫存
*/
public Integer getUsed() {
return used;
}
/**
* 設定已用庫存
*
* @param used 已用庫存
*/
public void setUsed(Integer used) {
this.used = used;
}
/**
* 獲取剩餘庫存
*
* @return residue - 剩餘庫存
*/
public Integer getResidue() {
return residue;
}
/**
* 設定剩餘庫存
*
* @param residue 剩餘庫存
*/
public void setResidue(Integer residue) {
this.residue = residue;
}
}
Mapper
Mapper介面
import com.atguigu.cloud.entities.Storage;
import org.apache.ibatis.annotations.Param;
import tk.mybatis.mapper.common.Mapper;
public interface StorageMapper extends Mapper<Storage> {
/**
* 扣減庫存
*/
void decrease(@Param("productId") Long productId, @Param("count") Integer count);
}
resources資料夾下新建mapper資料夾後新增Mapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<mapper namespace="com.atguigu.cloud.mapper.StorageMapper">
<resultMap id="BaseResultMap" type="com.atguigu.cloud.entities.Storage">
<!--
WARNING - @mbg.generated
-->
<id column="id" jdbcType="BIGINT" property="id" />
<result column="product_id" jdbcType="BIGINT" property="productId" />
<result column="total" jdbcType="INTEGER" property="total" />
<result column="used" jdbcType="INTEGER" property="used" />
<result column="residue" jdbcType="INTEGER" property="residue" />
</resultMap>
<update id="decrease">
UPDATE
t_storage
SET
used = used + #{count},
residue = residue - #{count}
WHERE product_id = #{productId}
</update>
</mapper>
Service介面及實現
public interface StorageService {
/**
* 扣減庫存
*/
void decrease(Long productId, Integer count);
}
import com.atguigu.cloud.mapper.StorageMapper;
import com.atguigu.cloud.service.StorageService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class StorageServiceImpl implements StorageService {
@Resource
private StorageMapper storageMapper;
/**
* 扣減庫存
*/
@Override
public void decrease(Long productId, Integer count) {
log.info("------->storage-service中扣減庫存開始");
storageMapper.decrease(productId, count);
log.info("------->storage-service中扣減庫存結束");
}
}
Controller
import com.atguigu.cloud.entities.Order;
import com.atguigu.cloud.resp.ResultData;
import com.atguigu.cloud.service.OrderService;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class OrderController {
@Resource
private OrderService orderService;
/**
* 建立訂單
*/
@GetMapping("/order/create")
public ResultData create(Order order)
{
orderService.create(order);
return ResultData.success(order);
}
}
5.5 新建賬戶Account微服務
5.5.1 建Module
seata-account-service2003
5.5.2 改POM
<dependencies>
<!-- nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--alibaba-seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--loadbalancer-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!--cloud_commons_utils-->
<dependency>
<groupId>com.atguigu.cloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--web + actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--SpringBoot整合druid連線池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<!-- Swagger3 呼叫方式 http://你的主機IP地址:5555/swagger-ui/index.html -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
<!--mybatis和springboot整合-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!--Mysql資料庫驅動8 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--persistence-->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
</dependency>
<!--通用Mapper4-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!-- fastjson2 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
<scope>provided</scope>
</dependency>
<!--test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
5.5.3 寫YML
server:
port: 2003
spring:
application:
name: seata-account-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服務註冊中心地址
# ==========applicationName + druid-mysql8 driver===================
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_account?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
username: root
password: xing
# ========================mybatis===================
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.atguigu.cloud.entities
configuration:
map-underscore-to-camel-case: true
# ========================seata===================
seata:
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: ""
group: SEATA_GROUP
application: seata-server
tx-service-group: default_tx_group # 事務組,由它獲得TC服務的叢集名稱
service:
vgroup-mapping:
default_tx_group: default # 事務組與TC服務叢集的對映關係
data-source-proxy-mode: AT
logging:
level:
io:
seata: info
5.5.4 主啟動
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import tk.mybatis.spring.annotation.MapperScan;
@EnableDiscoveryClient
@EnableFeignClients
@MapperScan("com.atguigu.cloud.mapper") //import tk.mybatis.spring.annotation.MapperScan;
@SpringBootApplication
public class SeataAccountMainApp2003 {
public static void main(String[] args) {
SpringApplication.run(SeataAccountMainApp2003.class, args);
}
}
5.5.5 業務類
entities
package com.atguigu.cloud.entities;
import lombok.ToString;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
/**
* 表名:t_account
*/
@Table(name = "t_account")
@ToString
public class Account {
@Id
@GeneratedValue(generator = "JDBC")
private String id;
private String username;
private Double balance;
/**
* @return id
*/
public String getId() {
return id;
}
/**
* @param id
*/
public void setId(String id) {
this.id = id;
}
/**
* @return username
*/
public String getUsername() {
return username;
}
/**
* @param username
*/
public void setUsername(String username) {
this.username = username;
}
/**
* @return balance
*/
public Double getBalance() {
return balance;
}
/**
* @param balance
*/
public void setBalance(Double balance) {
this.balance = balance;
}
}
Mapper
Mapper介面
package com.atguigu.cloud.mapper;
import com.atguigu.cloud.entities.Account;
import org.apache.ibatis.annotations.Param;
import tk.mybatis.mapper.common.Mapper;
public interface AccountMapper extends Mapper<Account> {
/**
* @param userId
* @param money 本次消費金額
*/
void decrease(@Param("userId") Long userId, @Param("money") Long money);
}
resources資料夾下新建mapper資料夾後新增Mapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.cloud.mapper.AccountMapper">
<resultMap id="BaseResultMap" type="com.atguigu.cloud.entities.Account">
<!--
WARNING - @mbg.generated
-->
<id column="id" jdbcType="VARCHAR" property="id" />
<result column="username" jdbcType="VARCHAR" property="username" />
<result column="balance" jdbcType="DOUBLE" property="balance" />
</resultMap>
<!--
money 本次消費金額
t_account資料庫表
total總額度 = 累計已消費金額(used) + 剩餘可用額度(residue)
-->
<update id="decrease">
UPDATE
t_account
SET
residue = residue - #{money},used = used + #{money}
WHERE user_id = #{userId};
</update>
</mapper>
Service介面及實現
package com.atguigu.cloud.service;
public interface AccountService {
/**
* 扣減賬戶餘額
* @param userId 使用者id
* @param money 本次消費金額
*/
void decrease(Long userId, Long money);
}
package com.atguigu.cloud.service.impl;
import com.atguigu.cloud.mapper.AccountMapper;
import com.atguigu.cloud.service.AccountService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
@Slf4j
public class AccountServiceImpl implements AccountService
{
@Resource
AccountMapper accountMapper;
/**
* 扣減賬戶餘額
*/
@Override
public void decrease(Long userId, Long money) {
log.info("------->account-service中扣減賬戶餘額開始");
accountMapper.decrease(userId,money);
//myTimeOut();
//int age = 10/0;
log.info("------->account-service中扣減賬戶餘額結束");
}
/**
* 模擬超時異常,全域性事務回滾
*/
private static void myTimeOut()
{
try { TimeUnit.SECONDS.sleep(65); } catch (InterruptedException e) { e.printStackTrace(); }
}
}
Controller
import com.atguigu.cloud.resp.ResultData;
import com.atguigu.cloud.service.AccountService;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class AccountController {
@Resource
AccountService accountService;
/**
* 扣減賬戶餘額
*/
@RequestMapping("/account/decrease")
public ResultData decrease(@RequestParam("userId") Long userId, @RequestParam("money") Long money){
accountService.decrease(userId,money);
return ResultData.success("扣減賬戶餘額成功!");
}
}
6.Seata案例實戰-測試
這裡建議看,周陽老師原影片測試,影片比較直觀
6.1 啟動服務
啟動Nacos,之後啟動Seata,之後啟動2001,2002,2003服務。
6.2 資料庫初始情況
見本文,中插入初始表的sql語句。略
6.3 正常下單
下訂單 -> 減庫存 -> 扣餘額 -> 修改訂單狀態。
先測試,沒有在訂單模組,新增@GlobalTransactional註解
http://localhost:2001/order/create?userId=1&productId=1&count=10&money=10
瀏覽器發起請求。
出現故障
解決方案,降低boot和cloud版本。
正常下單,第2次,正常執行。
新增一個訂單,和2個扣減。
6.4 超時異常出錯,未加@GlobalTransactional
修改
@Override
public void decrease(Long userId, Long money) {
log.info("------->account-service中扣減賬戶餘額開始");
accountMapper.decrease(userId,money);
myTimeOut();
//int age = 10/0;
log.info("------->account-service中扣減賬戶餘額結束");
}
重新下單,重新發起請求。
發現,庫存和賬戶金額扣減後,訂單狀態並沒有設定為已經完成,沒有從0改1。
6.5 超時異常解決,新增@GlobalTransactional
重新下單,保留了超時方法情況下。
檢視Seata的後臺,會發現,全域性事務id和全域性鎖資訊
超時結束後,3個庫的資料並無任何改變,被回滾了,包括記錄修改前後資訊的undo_log中的記錄也會被刪除。
7.Seata原理小總結
AT模式如何做到對業務的無侵入
這裡,強烈推薦,看Seata官網對於AT模式原理的介紹。
整體機制,是兩階段提交協議。
- 一階段:業務資料和回滾日誌記錄在同一個本地事務中提交,釋放本地鎖和連線資源。
- 二階段:
- 提交非同步化,非常快速的完成。
- 回滾透過一階段的回滾日誌進行反向補償(注意,所謂的回滾類似與MySQL中的undolog日誌,是在原有的資料基礎上,再執行記錄的與原先修改相反的sql語句,完成最終資料的回滾)。
只是為了記錄自己的學習歷程,且本人水平有限,不對之處,請指正。