Apache Seata

1.Seata 簡介

Seata 是一款開源的分布式事務(wù)解決方案,致力于提供高性能與簡單易用的分布式事務(wù)服務(wù),為用戶提供了 AT、TCC、SAGA 和 XA 幾種不同的事務(wù)模式:

  • AT模式:無侵入式的分布式事務(wù)解決方案,適合不希望對業(yè)務(wù)進(jìn)行改造的場景,但由于需要添加全局事務(wù)鎖,對影響高并發(fā)系統(tǒng)的性能。該模式主要關(guān)注多DB訪問的數(shù)據(jù)一致性,也包括多服務(wù)下的多DB數(shù)據(jù)訪問一致性問題
  • TCC模式:高性能的分布式事務(wù)解決方案,適用于對性能要求比較高的場景。該模式主要關(guān)注業(yè)務(wù)拆分,在按照業(yè)務(wù)橫向擴(kuò)展資源時,解決服務(wù)間調(diào)用的一致性問題
  • Saga模式:長事務(wù)的分布式事務(wù)解決方案,適用于業(yè)務(wù)流程長且需要保證事務(wù)最終一致性的業(yè)務(wù)系統(tǒng)。Saga 模式一階段就會提交本地事務(wù),無鎖,長流程情況下可以保證性能,多用于渠道層、集成層業(yè)務(wù)系統(tǒng),事務(wù)參與者可以是其它公司的服務(wù)也可以是遺留系統(tǒng)的服務(wù),并且對于無法進(jìn)行改造和提供 TCC 要求的接口,也可以使用 Saga 模式

2.Seata 的核心組件

在 Seata 中主要有以下三種角色,其中 TM 和 RM 是作為 Seata 的客戶端與業(yè)務(wù)系統(tǒng)集成在一起,TC 作為 Seata 的服務(wù)端獨立部署:

  • 事務(wù)協(xié)調(diào)器(TC):維護(hù)全局事務(wù)的運行狀態(tài),負(fù)責(zé)協(xié)調(diào)并驅(qū)動全局提交或回滾
  • 事務(wù)管理器(TM):事務(wù)發(fā)起方,控制全局事務(wù)的范圍,負(fù)責(zé)開啟一個全局事務(wù),并最終發(fā)起全局提交或回滾全局的決議
  • 資源管理器(RM):事務(wù)參與方,管理本地事務(wù)正在處理的資源,負(fù)責(zé)向 TC 注冊本地事務(wù)、匯報本地事務(wù)狀態(tài),接收 TC 的命令來驅(qū)動本地事務(wù)的提交或回滾

3.Seata 的 AT 模式原理

AT模式,是seata的默認(rèn)/獨有模式,也是實際項目中比較常用的一種模式,他采用的也是兩階段提交,不過彌補了XA模式中資源鎖定周期過長的缺點,相對于XA來說,性能更好一些,但缺點就是數(shù)據(jù)不是強一致,因為它的數(shù)據(jù)會真實的提交到數(shù)據(jù)庫的,而如果后面做分支事務(wù)有問題的話,回滾靠的是日志來實現(xiàn)最終一致。

基本原理流程圖:

image

階段一RM的工作:

  1. 先會注冊一個分支事務(wù)到事務(wù)協(xié)調(diào)者TC中
  2. 記錄一個SQL更新前的快照和一個更新后的快照到undo_log日志表中
  3. 執(zhí)行SQL并提交數(shù)據(jù)庫事務(wù)
  4. 報告事務(wù)狀態(tài)

階段二RM的工作:

  1. 如果此時所有微服務(wù)都執(zhí)行完,并且沒有出現(xiàn)異常情況,事務(wù)協(xié)調(diào)者TC通知RM刪除undo-log記錄。
  2. 如果此時中途有微服務(wù)出現(xiàn)異常情況,則TC會通知RM根據(jù)undo-log記錄的對應(yīng)快照恢復(fù)數(shù)據(jù)到更新前。

4.SpringCloud Alibaba 整合 Seata AT 模式

4.1.開發(fā)組件介紹

名稱 版本
HighGoDB 安全版V4.5
HgdbJdbc 42.5.0
JDK 1.8
Java IDE IntelliJ IDEA
spring-cloud Hoxton.SR10
spring-cloud-alibaba 2.2.5.RELEASE
SpringBoot 2.2.4.RELEASE
mybatis 7.0.0.Beta2
seata 1.4.2
nacos 2.2.0

4.2.安裝nacos

參考Nacos2.2.0適配瀚高數(shù)據(jù)庫

windows環(huán)境下,在nacos/bin目錄下,進(jìn)入dos輸入命令startup.cmd -m standalone啟動nacos

image

http://192.168.10.127:8848/nacos/index.html

用戶名:nacos

密碼:nacos

image

4.3.安裝seata

4.3.1.下載seata-server

https://github.com/seata/seata/releases

下載安裝包和源碼包

image

4.3.2.增加瀚高驅(qū)動支持

安裝包解壓后,seata-server-1.4.2\lib路徑下添加hgdb-pgjdbc-42.5.0.jar

驅(qū)動包下載地址

https://mvnrepository.com/artifact/com.highgo/hgdb-pgjdbc

4.3.3.創(chuàng)建相關(guān)數(shù)據(jù)庫和表

參考postgresql.sql文件,可在4.1.章節(jié)下載的源碼包incubator-seata-1.4.2\script\server\db路徑下找到。

--創(chuàng)建數(shù)據(jù)庫seata
create database seata;
--創(chuàng)建相關(guān)的表
CREATE TABLE IF NOT EXISTS public.global_table
(
xid VARCHAR(128) NOT NULL,
transaction_id BIGINT,
status SMALLINT 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 TIMESTAMP(0),
gmt_modified TIMESTAMP(0),
PRIMARY KEY (xid)
);

CREATE INDEX ON public.global_table (gmt_modified, status);
CREATE INDEX ON public.global_table (transaction_id);

CREATE TABLE IF NOT EXISTS public.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 SMALLINT,
client_id VARCHAR(64),
application_data VARCHAR(2000),
gmt_create TIMESTAMP(6),
gmt_modified TIMESTAMP(6),
PRIMARY KEY (branch_id)
);

CREATE INDEX ON public.branch_table (xid);

CREATE TABLE IF NOT EXISTS public.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),
gmt_create TIMESTAMP(0),
gmt_modified TIMESTAMP(0),
PRIMARY KEY (row_key)
);

CREATE INDEX ON public.lock_table (branch_id);
4.3.4.配置相關(guān)
  1. 自定義命名空間

為了方便區(qū)分不同的應(yīng)用,在nacos新建命名空間seata,這里創(chuàng)建后的命名空間,這里的命名空間id會在之后的配置中使用到

image

  1. 配置seataServer.properties

image

Data ID: seataServer.properties

Group: SEATA_GROUP

配置格式 Properties

注意:在這里配置的Data ID 和 Group 需要和后文中配置保持一致,以確保seata能拿到存放在nacos中的配置

image

配置內(nèi)容,參考配置文件config.txt,可在4.1.章節(jié)下載的源碼包incubator-seata-1.4.2\script\config-center下找到。

transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableClientBatchSendRequest=false
transport.threadFactory.bossThreadPrefix=NettyBoss
transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
transport.threadFactory.shareBossWorker=false
transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
transport.threadFactory.clientSelectorThreadSize=1
transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
transport.threadFactory.bossThreadSize=1
transport.threadFactory.workerThreadSize=default
transport.shutdown.wait=3
#default_tx_group和值均可修改,此處使用默認(rèn),后文中客戶端配置seata時要與該配置保持一致
service.vgroupMapping.default_tx_group=default
service.default.grouplist=127.0.0.1:8091
service.enableDegrade=false
service.disableGlobalTransaction=false
client.rm.asyncCommitBufferLimit=10000
client.rm.lock.retryInterval=10
client.rm.lock.retryTimes=30
client.rm.lock.retryPolicyBranchRollbackOnConflict=true
client.rm.reportRetryCount=5
client.rm.tableMetaCheckEnable=false
client.rm.tableMetaCheckerInterval=60000
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.defaultGlobalTransactionTimeout=60000
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
#model改為db
store.mode=db
store.publicKey=
store.file.dir=file_store/data
store.file.maxBranchSessionSize=16384
store.file.maxGlobalSessionSize=512
store.file.fileWriteBufferCacheSize=16384
store.file.flushDiskMode=async
store.file.sessionReloadReadSize=100
store.db.datasource=druid
#數(shù)據(jù)庫類型改為postgresql
store.db.dbType=postgresql
#修改數(shù)據(jù)庫驅(qū)動,這里使用瀚高
store.db.driverClassName=org.postgresql.Driver
#修改為已創(chuàng)建的seatas數(shù)據(jù)庫
store.db.url=jdbc:postgresql://192.168.100.101:5866/seata
#修改數(shù)據(jù)庫用戶名
store.db.user=sysdba
#修改數(shù)據(jù)庫密碼
store.db.password=Qwer@1234
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
store.redis.mode=single
store.redis.single.host=127.0.0.1
store.redis.single.port=6379
store.redis.sentinel.masterName=
store.redis.sentinel.sentinelHosts=
store.redis.maxConn=10
store.redis.minConn=1
store.redis.maxTotal=100
store.redis.database=0
store.redis.password=
store.redis.queryLimit=100
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.onlyCareUpdateColumns=true
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
client.undo.logTable=undo_log
client.undo.compress.enable=true
client.undo.compress.type=zip
client.undo.compress.threshold=64k
log.exceptionRate=100
transport.serialization=seata
transport.compressor=none
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898
  1. 配置seata-server-1.4.2\conf\file.conf
## transaction log store, only used in seata-server
store {
## store mode: file、db、redis
mode = "db"

## database store property
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
datasource = "druid"
## mysql/oracle/postgresql/h2/oceanbase etc.
dbType = "postgresql"
driverClassName = "org.postgresql.Driver"
## if using mysql to store the data, recommend add rewriteBatchedStatements=true in jdbc connection param
url = "jdbc:postgresql://192.168.100.101:5866/seata"
user = "sysdba"
password = "Qwer@1234"
minConn = 5
maxConn = 100
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
maxWait = 5000
}
}

office

  1. 配置seata-server-1.4.2\conf\registry.conf

注意:

namespace 對應(yīng)nacos中自定義的seata命名空間ID

group 對應(yīng)nacos中配置的 Group(SEATA_GROUP)

dataId 對應(yīng)nacos中配置的 Data ID(seataServer.properties)

registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos" #改為nacos

nacos {
application = "seata-server"
#改為自己的nacos地址
serverAddr = "127.0.0.1:8848"
#改為自己的group
group = "SEATA_GROUP"
#改為自己的nacos的namespace,這里填寫的是剛才創(chuàng)建seata命名空間的id
namespace = "31976cce-fc16-4c7f-8c39-74f78ebe50a3"
cluster = "default"
#改為自己nacos的用戶名
username = "nacos"
#改為自己nacos的密碼
password = "nacos"
}
}

config {
# file、nacos 、apollo、zk、consul、etcd3
type = "nacos" #改為nacos

nacos {
#改為自己的nacos地址
serverAddr = "127.0.0.1:8848"
#改為自己的nacos的namespace,這里填寫的是剛才創(chuàng)建seata命名空間的id
namespace = "31976cce-fc16-4c7f-8c39-74f78ebe50a3"
group = "SEATA_GROUP"
#改為自己nacos的用戶名
username = "nacos"
#改為自己nacos的密碼
password = "nacos"
#改為自己的配置文件dataId
dataId = "seataServer.properties"
}
}

office

4.3.5.啟動seata

cmd啟動seata-server.bat

image

如圖所示,沒報錯就是啟動成功了。

image

nacos服務(wù)列表中可以看到注冊的seata服務(wù)。由于我們配置命名空間為seata,因此在seata命名空間里能看到此服務(wù)。

image

4.4.項目搭建

創(chuàng)建三個項目分別負(fù)責(zé)下單,庫存,賬戶余額進(jìn)行演示分布式事務(wù)。

4.4.1.數(shù)據(jù)庫準(zhǔn)備

在seata庫下分別創(chuàng)建三個模式,建表。

seata_order

CREATE SCHEMA seata_order;

CREATE TABLE seata_order.t_order (
id bigserial NOT NULL,
user_id bigint DEFAULT NULL,
product_id bigint DEFAULT NULL,
count int DEFAULT NULL,
money NUMERIC(11, 0) DEFAULT NULL,
status int DEFAULT NULL,
PRIMARY KEY (id)
);

COMMENT ON COLUMN seata_order.t_order.user_id IS '用戶id';
COMMENT ON COLUMN seata_order.t_order.product_id IS '產(chǎn)品id';
COMMENT ON COLUMN seata_order.t_order.count IS '數(shù)量';
COMMENT ON COLUMN seata_order.t_order.money IS '金額';
COMMENT ON COLUMN seata_order.t_order.status IS '訂單狀態(tài):0:創(chuàng)建中;1:已完結(jié)';

CREATE TABLE IF NOT EXISTS seata_order.undo_log
(
id bigserial NOT NULL,
branch_id BIGINT NOT NULL,
xid VARCHAR(100) NOT NULL,
context VARCHAR(128) NOT NULL,
rollback_info bytea NOT NULL,
log_status INT NOT NULL,
log_created TIMESTAMP NOT NULL,
log_modified TIMESTAMP NOT NULL,
PRIMARY KEY (id),
UNIQUE (xid, branch_id)
);

seata_storage

CREATE SCHEMA seata_storage;

CREATE TABLE seata_storage.t_storage (
id bigserial NOT NULL,
product_id bigint DEFAULT NULL,
total int DEFAULT NULL,
used int DEFAULT NULL,
residue int DEFAULT NULL,
PRIMARY KEY (id)
);

COMMENT ON COLUMN seata_storage.t_storage.product_id IS '產(chǎn)品id';
COMMENT ON COLUMN seata_storage.t_storage.total IS '庫存';
COMMENT ON COLUMN seata_storage.t_storage.used IS '已用庫存';
COMMENT ON COLUMN seata_storage.t_storage.residue IS '剩余庫存';

INSERT INTO seata_storage.t_storage (id, product_id, total, used, residue) VALUES (1, 1, 100, 0, 100);

CREATE TABLE IF NOT EXISTS seata_storage.undo_log
(
id bigserial NOT NULL,
branch_id BIGINT NOT NULL,
xid VARCHAR(100) NOT NULL,
context VARCHAR(128) NOT NULL,
rollback_info bytea NOT NULL,
log_status INT NOT NULL,
log_created TIMESTAMP NOT NULL,
log_modified TIMESTAMP NOT NULL,
PRIMARY KEY (id),
UNIQUE (xid, branch_id)
);

seata_account

CREATE SCHEMA seata_account;

CREATE TABLE IF NOT EXISTS seata_account.t_account (
id bigserial NOT NULL,
user_id bigint DEFAULT NULL,
total NUMERIC(10, 0) DEFAULT NULL,
used NUMERIC(10, 0) DEFAULT NULL,
residue NUMERIC(10, 0) DEFAULT NULL,
PRIMARY KEY (id)
);

COMMENT ON COLUMN seata_account.t_account.user_id IS '用戶id';
COMMENT ON COLUMN seata_account.t_account.total IS '總額度';
COMMENT ON COLUMN seata_account.t_account.used IS '已用額度';
COMMENT ON COLUMN seata_account.t_account.residue IS '剩余可用額度';

INSERT INTO seata_account.t_account (id, user_id, total, used, residue) VALUES (1, 1, 1000, 0, 1000);

CREATE TABLE IF NOT EXISTS seata_account.undo_log
(
id bigserial NOT NULL,
branch_id BIGINT NOT NULL,
xid VARCHAR(100) NOT NULL,
context VARCHAR(128) NOT NULL,
rollback_info bytea NOT NULL,
log_status INT NOT NULL,
log_created TIMESTAMP NOT NULL,
log_modified TIMESTAMP NOT NULL,
PRIMARY KEY (id),
UNIQUE (xid, branch_id)
);

image

4.4.2.父工程springcloud-seata-demo

image

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.2.4.RELEASE</version>
</parent>
<groupId>com.highgo.seata</groupId>
<artifactId>springcloud-seata-demo</artifactId>
<version>1.0.1</version>
<packaging>pom</packaging>

<modules>
<module>seata-order</module>
<module>seata-storage</module>
<module>seata-account</module>
</modules>

<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-cloud.version>Hoxton.SR10</spring-cloud.version>
<spring-cloud-alibaba.version>2.2.5.RELEASE</spring-cloud-alibaba.version>
<mybatis.spring.boot.version>2.1.4</mybatis.spring.boot.version>
<seata.version>1.4.2</seata.version>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.spring.boot.version}</version>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>${seata.version}</version>
</dependency>
<dependency>
<groupId>com.highgo</groupId>
<artifactId>HgdbJdbc</artifactId>
<version>6.2.4</version>
</dependency>
<dependency>
<groupId>com.highgo</groupId>
<artifactId>hgdb-pgjdbc</artifactId>
<version>42.5.0</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
4.4.3.子工程seata-order

image

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>springcloud-seata-demo</artifactId>
<groupId>com.highgo.seata</groupId>
<version>1.0.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>seata-order</artifactId>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>6</source>
<target>6</target>
</configuration>
</plugin>
</plugins>
</build>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.highgo</groupId>
<artifactId>HgdbJdbc</artifactId>
</dependency>
<dependency>
<groupId>com.highgo</groupId>
<artifactId>hgdb-pgjdbc</artifactId>
</dependency>
</dependencies>
</project>

application.yml

server:
port: 2001
spring:
application:
name: order-service
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
username: nacos
password: nacos
group: SEATA_GROUP
namespace: 31976cce-fc16-4c7f-8c39-74f78ebe50a3

datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://192.168.100.101:5866/seata?currentSchema=seata_order
username: sysdba
password: Qwer@1234

seata:
enable-auto-data-source-proxy: false
registry:
type: nacos
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
username: ${spring.cloud.nacos.discovery.username}
password: ${spring.cloud.nacos.discovery.password}
group: ${spring.cloud.nacos.discovery.group}
namespace: ${spring.cloud.nacos.discovery.namespace}
config:
type: nacos
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
username: ${spring.cloud.nacos.discovery.username}
password: ${spring.cloud.nacos.discovery.password}
group: ${spring.cloud.nacos.discovery.group}
namespace: ${spring.cloud.nacos.discovery.namespace}
data-id: seataServer.properties

tx-service-group: default_tx_group # 事務(wù)組的名稱,對應(yīng)service.vgroupMapping.default_tx_group=xxx中配置的default_tx_group
service:
vgroup-mapping:
default_tx_group: default #指定事務(wù)分組至集群映射關(guān)系(等號右側(cè)的集群名需要與Seata-server注冊到Nacos的cluster保持一致)

feign:
hystrix:
enabled: true

logging:
level:
io:
seata: info

mybatis:
mapper-locations: classpath*:mapper/*.xml

config配置類

MyBatisConfig

package com.highgo.seata.order.config;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@MapperScan("com.highgo.seata.order.dao")
public class MyBatisConfig {
}

DataSourceProxyConfig

package com.highgo.seata.order.config;

import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DataSourceProxyConfig {
@Value("${spring.datasource.driver-class-name}")
private String driverClassName;
@Value("${spring.datasource.url}")
private String url;
@Value("${spring.datasource.username}")
private String username;
@Value("${spring.datasource.password}")
private String password;

/**
* 將阿里云提供的seata專用數(shù)據(jù)庫連接池對象放入IOC容器
*/
@Bean
public DataSourceProxy dataSource(){
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setDriverClassName(driverClassName);
druidDataSource.setUrl(url);
druidDataSource.setUsername(username);
druidDataSource.setPassword(password);
return new DataSourceProxy(druidDataSource);
}

}

實體類

CommonResult

package com.highgo.seata.order.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T>
{
private Integer code;
private String message;
private T data;

public CommonResult(Integer code, String message)
{
this(code,message,null);
}
}

Order

package com.highgo.seata.order.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order {

private Long id;
private Long userId;
private Long productId;
private Integer count;
private BigDecimal money;
/**
* 訂單狀態(tài):0:創(chuàng)建中;1:已完結(jié)
*/
private Integer status;
}

Dao層

OrderDao

package com.highgo.seata.order.dao;

import com.highgo.seata.order.domain.Order;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper
public interface OrderDao {

/**
* 創(chuàng)建訂單
*/
void create(Order order);

/**
* 修改訂單金額
*/
void update(@Param("userId") Long userId,@Param("status")Integer status);

}

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.highgo.seata.order.dao.OrderDao">

<resultMap id="BaseResultMap" type="com.highgo.seata.order.domain.Order">
<id column="id" property="id" jdbcType="BIGINT"></id>
<result column="user_id" property="userId" jdbcType="BIGINT"></result>
<result column="product_id" property="productId" jdbcType="BIGINT"></result>
<result column="count" property="count" jdbcType="INTEGER"></result>
<result column="money" property="money" jdbcType="DECIMAL"></result>
<result column="status" property="status" jdbcType="INTEGER"></result>
</resultMap>

<insert id="create" parameterType="com.highgo.seata.order.domain.Order">
insert into t_order(user_id,product_id,count,money,status) values (#{userId},#{productId},#{count},#{money},0);
</insert>

<update id="update">
update t_order set status =1 where user_id =#{userId} and status=#{status};
</update>
</mapper>

service業(yè)務(wù)類

OrderService

package com.highgo.seata.order.service;

import com.highgo.seata.order.domain.Order;

public interface OrderService {

void create(Order order);

}

StorageService

package com.highgo.seata.order.service;

import com.highgo.seata.order.domain.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(value = "storage-service")
public interface StorageService {

@PostMapping("/storage/decrease")
CommonResult decrease(@RequestParam("productId") Long productId,
@RequestParam("count") Integer count);

}

AccountService

package com.highgo.seata.order.service;

import com.highgo.seata.order.domain.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.math.BigDecimal;

@FeignClient(value = "account-service")
public interface AccountService {
/**
* 扣減賬戶余額
*/
@PostMapping("/account/decrease")
CommonResult decrease(@RequestParam("userId") Long userId,
@RequestParam("money") BigDecimal money);

}

OrderServiceImpl

package com.highgo.seata.order.service.impl;

import com.highgo.seata.order.dao.OrderDao;
import com.highgo.seata.order.domain.Order;
import com.highgo.seata.order.service.AccountService;
import com.highgo.seata.order.service.OrderService;
import com.highgo.seata.order.service.StorageService;
import io.seata.core.context.RootContext;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
@Slf4j
public class OrderServiceImpl implements OrderService {

@Resource
private OrderDao orderDao;
@Resource
private AccountService accountService;
@Resource
private StorageService storageService;

@Override
@GlobalTransactional(name = "create-order",rollbackFor = Exception.class)
public void create(Order order) {
log.info("order-service事務(wù)id---------------------->"+ RootContext.getXID());

log.info("------->下單開始");
//本應(yīng)用創(chuàng)建訂單
orderDao.create(order);

//遠(yuǎn)程調(diào)用庫存服務(wù)扣減庫存
log.info("------->order-service中扣減庫存開始");
storageService.decrease(order.getProductId(),order.getCount());
log.info("------->order-service中扣減庫存結(jié)束");

//遠(yuǎn)程調(diào)用賬戶服務(wù)扣減余額
log.info("------->order-service中扣減余額開始");
accountService.decrease(order.getUserId(),order.getMoney());
log.info("------->order-service中扣減余額結(jié)束");

//修改訂單狀態(tài)為已完成
log.info("------->order-service中修改訂單狀態(tài)開始");
orderDao.update(order.getUserId(),0);
log.info("------->order-service中修改訂單狀態(tài)結(jié)束");

log.info("------->下單結(jié)束");
}
}

controller控制層

OrderController

package com.highgo.seata.order.controller;

import com.highgo.seata.order.domain.CommonResult;
import com.highgo.seata.order.domain.Order;
import com.highgo.seata.order.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/order")
public class OrderController {

@Autowired
private OrderService orderService;

@GetMapping("/create")
public CommonResult create(Order order) {
orderService.create(order);
return new CommonResult(200, "訂單創(chuàng)建完成");
}
}

主啟動類

package com.highgo.seata.order;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class OrderApplication {

public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}

}
4.4.4.子工程seata-storage

image

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>springcloud-seata-demo</artifactId>
<groupId>com.highgo.seata</groupId>
<version>1.0.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>seata-storage</artifactId>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.highgo</groupId>
<artifactId>HgdbJdbc</artifactId>
</dependency>
<dependency>
<groupId>com.highgo</groupId>
<artifactId>hgdb-pgjdbc</artifactId>
</dependency>
</dependencies>
</project>

application.yml

server:
port: 2002
spring:
application:
name: storage-service
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
username: nacos
password: nacos
group: SEATA_GROUP
namespace: 31976cce-fc16-4c7f-8c39-74f78ebe50a3

datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://192.168.100.101:5866/seata?currentSchema=seata_storage
username: sysdba
password: Qwer@1234

seata:
enable-auto-data-source-proxy: false
registry:
type: nacos
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
username: ${spring.cloud.nacos.discovery.username}
password: ${spring.cloud.nacos.discovery.password}
group: ${spring.cloud.nacos.discovery.group}
namespace: ${spring.cloud.nacos.discovery.namespace}
config:
type: nacos
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
username: ${spring.cloud.nacos.discovery.username}
password: ${spring.cloud.nacos.discovery.password}
group: ${spring.cloud.nacos.discovery.group}
namespace: ${spring.cloud.nacos.discovery.namespace}
data-id: seataServer.properties

tx-service-group: default_tx_group # 事務(wù)組的名稱,對應(yīng)service.vgroupMapping.default_tx_group=xxx中配置的default_tx_group
service:
vgroup-mapping:
default_tx_group: default #指定事務(wù)分組至集群映射關(guān)系(等號右側(cè)的集群名需要與Seata-server注冊到Nacos的cluster保持一致)

feign:
hystrix:
enabled: true

logging:
level:
io:
seata: info

mybatis:
mapper-locations: classpath*:mapper/*.xml

config配置類

MyBatisConfig

package com.highgo.seata.storage.config;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@MapperScan("com.highgo.seata.storage.dao")
public class MyBatisConfig {
}

DataSourceProxyConfig

package com.highgo.seata.storage.config;

import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DataSourceProxyConfig {
@Value("${spring.datasource.driver-class-name}")
private String driverClassName;
@Value("${spring.datasource.url}")
private String url;
@Value("${spring.datasource.username}")
private String username;
@Value("${spring.datasource.password}")
private String password;

/**
* 將阿里云提供的seata專用數(shù)據(jù)庫連接池對象放入IOC容器
*/
@Bean
public DataSourceProxy dataSource(){
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setDriverClassName(driverClassName);
druidDataSource.setUrl(url);
druidDataSource.setUsername(username);
druidDataSource.setPassword(password);
return new DataSourceProxy(druidDataSource);
}

}

實體類

Storage

package com.highgo.seata.storage.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Storage {

private Long id;
private Long productId;
private Integer total;
private Integer used;
private Integer residue;

}

CommonResult

package com.highgo.seata.storage.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T>
{
private Integer code;
private String message;
private T data;

public CommonResult(Integer code, String message)
{
this(code,message,null);
}
}

Dao層

StorageDao

package com.highgo.seata.storage.dao;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper
public interface StorageDao {
void decrease(@Param("productId") Long productId, @Param("count") Integer count);

}

StorageMapper.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.highgo.seata.storage.dao.StorageDao">

<resultMap id="BaseResultMap" type="com.highgo.seata.storage.domain.Storage">
<id column="id" property="id" jdbcType="BIGINT"></id>
<result column="product_id" property="productId" jdbcType="BIGINT"></result>
<result column="total" property="total" jdbcType="INTEGER"></result>
<result column="used" property="used" jdbcType="INTEGER"></result>
<result column="residue" property="residue" jdbcType="INTEGER"></result>
</resultMap>

<!--減庫存-->
<update id="decrease">
update t_storage
set used =used + #{count},residue=residue - #{count}
where product_id=#{productId};
</update>
</mapper>

service業(yè)務(wù)類

StorageService

package com.highgo.seata.storage.service;

public interface StorageService {
/**
* 扣減庫存
*/
void decrease(Long productId, Integer count);

}

StorageServiceImpl

package com.highgo.seata.storage.service.impl;

import com.highgo.seata.storage.dao.StorageDao;
import com.highgo.seata.storage.service.StorageService;
import io.seata.core.context.RootContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
@Slf4j
public class StorageServiceImpl implements StorageService {
@Resource
private StorageDao storageDao;

@Override
public void decrease(Long productId, Integer count) {
log.info("storage-service事務(wù)id---------------------->"+ RootContext.getXID());
log.info("------->storage-service中扣減庫存開始");
storageDao.decrease(productId,count);
log.info("------->storage-service中扣減庫存結(jié)束");
}

}

Controller層

StorageController

package com.highgo.seata.storage.controller;

import com.highgo.seata.storage.domain.CommonResult;
import com.highgo.seata.storage.service.StorageService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequestMapping("/storage")
public class StorageController {

@Autowired
private StorageService storageService;

@PostMapping(value = "/decrease")
public CommonResult decrease(Long productId, Integer count) {
storageService.decrease(productId,count);
return new CommonResult(200, "扣減庫存成功!");
}

}

主啟動類

package com.highgo.seata.storage;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class StorageApplication {

public static void main(String[] args) {
SpringApplication.run(StorageApplication.class, args);
}

}
4.4.5.子工程seata-account

image

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>springcloud-seata-demo</artifactId>
<groupId>com.highgo.seata</groupId>
<version>1.0.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>seata-account</artifactId>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.highgo</groupId>
<artifactId>HgdbJdbc</artifactId>
</dependency>
<dependency>
<groupId>com.highgo</groupId>
<artifactId>hgdb-pgjdbc</artifactId>
</dependency>
</dependencies>
</project>

application.yml

server:
port: 2003
spring:
application:
name: account-service
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
username: nacos
password: nacos
group: SEATA_GROUP
namespace: 31976cce-fc16-4c7f-8c39-74f78ebe50a3

datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://192.168.100.101:5866/seata?currentSchema=seata_account
username: sysdba
password: Qwer@1234

seata:
enable-auto-data-source-proxy: false
registry:
type: nacos
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
username: ${spring.cloud.nacos.discovery.username}
password: ${spring.cloud.nacos.discovery.password}
group: ${spring.cloud.nacos.discovery.group}
namespace: ${spring.cloud.nacos.discovery.namespace}
config:
type: nacos
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
username: ${spring.cloud.nacos.discovery.username}
password: ${spring.cloud.nacos.discovery.password}
group: ${spring.cloud.nacos.discovery.group}
namespace: ${spring.cloud.nacos.discovery.namespace}
data-id: seataServer.properties

tx-service-group: default_tx_group # 事務(wù)組的名稱,對應(yīng)service.vgroupMapping.default_tx_group=xxx中配置的default_tx_group
service:
vgroup-mapping:
default_tx_group: default #指定事務(wù)分組至集群映射關(guān)系(等號右側(cè)的集群名需要與Seata-server注冊到Nacos的cluster保持一致)

feign:
hystrix:
enabled: true

logging:
level:
io:
seata: info

mybatis:
mapper-locations: classpath*:mapper/*.xml

config配置類

MybatisConfig

package com.highgo.seata.account.config;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@MapperScan("com.highgo.seata.account.dao")
public class MybatisConfig {
}

DataSourceProxyConfig

package com.highgo.seata.account.config;

import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DataSourceProxyConfig {
@Value("${spring.datasource.driver-class-name}")
private String driverClassName;
@Value("${spring.datasource.url}")
private String url;
@Value("${spring.datasource.username}")
private String username;
@Value("${spring.datasource.password}")
private String password;

/**
* 將阿里云提供的seata專用數(shù)據(jù)庫連接池對象放入IOC容器
*/
@Bean
public DataSourceProxy dataSource(){
DruidDataSource druidDataSource = new DruidDataSource();
druidDataSource.setDriverClassName(driverClassName);
druidDataSource.setUrl(url);
druidDataSource.setUsername(username);
druidDataSource.setPassword(password);
return new DataSourceProxy(druidDataSource);
}

}

實體類

Account

package com.highgo.seata.account.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {

private Long id;
private Long userId;
private Integer total;
private Integer used;
private Integer residue;
}

CommonResult

package com.highgo.seata.account.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T>
{
private Integer code;
private String message;
private T data;

public CommonResult(Integer code, String message)
{
this(code,message,null);
}
}

Dao層

AccountDao

package com.highgo.seata.account.dao;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.math.BigDecimal;

@Mapper
public interface AccountDao {
/**
* 扣減賬戶余額
*/
void decrease(@Param("userId") Long userId,@Param("money") BigDecimal money);

}

AccountMapper.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.highgo.seata.account.dao.AccountDao">
<resultMap id="BaseResultMap" type="com.highgo.seata.account.domain.Account">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="user_id" property="userId" jdbcType="BIGINT"/>
<result column="total" property="total" jdbcType="DECIMAL"/>
<result column="used" property="used" jdbcType="DECIMAL"/>
<result column="residue" property="residue" jdbcType="DECIMAL"/>
</resultMap>
<update id="decrease">
update t_account set residue = residue- #{money},used = used + #{money}
where user_id =#{userId}
</update>
</mapper>

service層

AccountService

package com.highgo.seata.account.service;

import org.springframework.web.bind.annotation.RequestParam;

import java.math.BigDecimal;

public interface AccountService {

void decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);

}

AccountServiceImpl

package com.highgo.seata.account.service.impl;

import com.highgo.seata.account.dao.AccountDao;
import com.highgo.seata.account.service.AccountService;
import io.seata.core.context.RootContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.math.BigDecimal;

@Service
@Slf4j
public class AccountServiceImpl implements AccountService {
@Resource
private AccountDao accountDao;

@Override
public void decrease(Long userId, BigDecimal money) {
log.info("account-service事務(wù)id---------------------->"+ RootContext.getXID());
log.info("------->account-service中扣減賬戶余額開始");
// try {
// //模擬超時異常,全局事務(wù)回滾
// TimeUnit.SECONDS.sleep(30000);
// } catch (Exception e) {
// e.printStackTrace();
// }
accountDao.decrease(userId, money);
log.info("------->account-service中扣減賬戶余額結(jié)束");
}
}

Controller層

AccountController

package com.highgo.seata.account.controller;

import com.highgo.seata.account.domain.CommonResult;
import com.highgo.seata.account.service.AccountService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.math.BigDecimal;
@Slf4j
@RestController
@RequestMapping("/account")
public class AccountController {

@Resource
private AccountService accountService;

@PostMapping("/decrease")
public CommonResult decrease(@RequestParam("userId") Long userId,
@RequestParam("money") BigDecimal money) {
accountService.decrease(userId, money);
return new CommonResult(200, "扣減賬戶余額成功!");
}

}

主啟動類

package com.highgo.seata.account;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class AccountApplication {

public static void main(String[] args) {
SpringApplication.run(AccountApplication.class, args);
}

}

5.場景測試

5.1.服務(wù)啟動

依次啟動Nacos、Seata、所有微服務(wù)

image

nacos服務(wù)列表

image

5.2.測試正常流程

服務(wù)seata-order打個斷點

image

瀏覽器訪問 http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100

當(dāng)進(jìn)到斷點時,數(shù)據(jù)庫中可以看到,seata_order、seata_storage、seata_account模式下的undo_log表,各自生成了一條日志記錄,全局事務(wù)xid相同 ,說明在同一個全局事務(wù)中。

image

查看訂單表、庫存表和余額表,數(shù)據(jù)均已提交

image

放開斷點,日志記錄表被刪除

image

日志輸出

image

5.3.測試回滾流程

服務(wù)seata-account,模擬超時異常

image

服務(wù)seata-order,打個斷點

image

瀏覽器訪問 http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100

當(dāng)進(jìn)入斷點扣減余額時,訂單和庫存對應(yīng)的日志表,各生成了一條記錄,全局事務(wù)xid是相同的

image

訂單表和庫存表,訂單已經(jīng)創(chuàng)建,庫存扣減完成

image

放開斷點,全局事務(wù)回滾

image

日志輸出

image