Apache Seata学习笔记分享

Seata是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。


目录

事务模式

AT模式

使用前提

整体机制

使用说明

TCC模式

模式优点

模式缺点

适用场景

使用说明

SAGA模式

模式优点

模式缺点

适用场景

XA模式

模式优点

模式缺点

适用场景

整体机制

模式总结

使用Seata

启动Seata服务器

1、下载软件包

2、修改软件配置

3、上传配置文件

4、运行启动脚本

5、创建Seata数据库

6、创建UNDO_LOG表

创建微服务应用

创建父项目

创建子模块

库存服务(stock-service)

stock-service-interface

pm.xml

Stock.java

StockService.java

stock-service-provider

pom.xml

application.yml

StockMapper.java

StockServiceImpl.java

账户服务(account-service)

account-service-interface

pom.xml

Account.java

AccountService.java

account-service-provider

pom.xml

application.yml

AccountMapper.java

AccountServiceImpl.java

订单服务(order-service)

order-service-interface

pom.xml

Order.java

OrderService.java

order-service-provider

pom.xml

application.yml

IdGenerator.java

OrderMapper.java

OrderServiceImpl.java

业务服务器(business-server)

pom.xml

application.yml

BusinessService.java

BusinessController.java

BusinessServiceImpl.java

启动微服务应用

成功的事务

失败的事务


事务模式

Seata为用户提供了AT、TCC、SAGA和XA事务模式,为用户打造一站式的分布式解决方案。

AT模式

AT模式是Seata创新的一种非侵入式的分布式事务解决方案,Seata 在内部做了对数据库操作的代理层。

我们使用Seata AT模式时,实际上用的是Seata自带的数据源代理DataSourceProxy。

Seata在这层代理中加入了很多逻辑,比如:插入回滚undo_log日志,检查全局锁等。

使用前提

由于AT模式通过创建数据源DataSource的代理类来实现的全局事务,所以使用有限制。

  • 基于支持本地ACID事务的关系型数据库。
  • Java 应用,通过JDBC访问数据库。

整体机制

两阶段提交协议的演变:

  • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。

  • 二阶段:

    • 提交异步化,非常快速地完成。

    • 回滚通过一阶段的回滚日志进行反向补偿。

一阶段本地事务提交前,需要确保先拿到全局锁

  • 拿不到全局锁,不能提交本地事务。

  • 拿全局锁的尝试被限制在一定范围内。

    • 超出范围将放弃,并回滚本地事务,释放本地锁。

使用说明

代码无侵入:只需要在业务方法上使用@GlobalTransactional开启全局事务。

@Service
public class BusinessServiceImpl implements BusinessService {

    @DubboReference
    private StockService stockService;

    @DubboReference
    private OrderService orderService;

    @GlobalTransactional
    @Override
    public void purchase(String userId, String productId, Integer orderCount) {
        stockService.deduct(productId, orderCount);

        orderService.create(userId, productId, orderCount);
    }

}

TCC模式

TCC(Try-Confirm-Cancel)模式是Seata支持的一种由业务方细粒度控制的侵入式分布式事务解决方案,是继AT模式后第二种支持的事务模式。

其分布式事务模型直接作用于服务层,不依赖底层数据库,可以灵活选择业务资源的锁定粒度,减少资源锁持有时间,可扩展性好,可以说是为独立部署的SOA服务而设计的。

模式优点

TCC 完全不依赖底层数据库,能够实现跨数据库、跨应用资源管理,可以提供给业务方更细粒度的控制。

模式缺点

TCC 是一种侵入式的分布式事务解决方案,需要业务系统自行实现Try、Confirm、Cancel三个操作,对业务系统有着非常大的入侵性,设计相对复杂。

适用场景

TCC模式是高性能分布式事务解决方案,适用于核心系统等对性能有很高要求的场景。

使用说明

需要为所有分支事务的方法定义提交和回滚操作。

public interface TccAction {
    
    @TwoPhaseBusinessAction(name = "TccAction", commitMethod = "commit", rollbackMethod = "rollback")
    public boolean prepare(BusinessActionContext actionContext);
    
    public boolean commit(BusinessActionContext actionContext);
    
    public boolean rollback(BusinessActionContext actionContext);
}

SAGA模式

Saga模式是SEATA提供的长事务解决方案,在Saga模式中,业务流程中每个参与者都提交本地事务。

当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。

模式优点

  • 一阶段提交本地事务,无锁,高性能

  • 事件驱动架构,参与者可异步执行,高吞吐

  • 补偿服务易于实现

模式缺点

不保证隔离性(应对方案见后面文档)

适用场景

  • 业务流程长、业务流程多

  • 参与者包含其它公司或遗留系统服务,无法提供TCC模式要求的三个接口

XA模式

所有分支事务都执行成功后,才提交事务;否则所有分支事务都会回滚。

XA模式会有阻塞等待阶段,等待其他分支事务正确执行完成后,通知其他分支事务提交。

代码无侵入:XA 模式使用起来与 AT 模式基本一致。

用法上的唯一区别在于数据源代理的替换:使用 DataSourceProxyXA 来替代 DataSourceProxy

 @Bean("dataSourceProxy")
 public DataSource dataSource(DruidDataSource druidDataSource) {
     return new DataSourceProxyXA(druidDataSource);
 }

模式优点

与Seata支持的其它事务模式不同,XA协议要求事务资源本身提供对规范和协议的支持。

所以事务资源(如数据库)可以保障从任意视角对数据的访问有效隔离,满足全局数据一致性。

此外的一些优势还包括:

  • 业务无侵入:和AT一样,XA模式将是业务无侵入的,不给应用设计和开发带来额外负担。

  • 数据库的支持广泛:XA协议被主流关系型数据库广泛支持,不需要额外的适配即可使用。

模式缺点

XA prepare 后,分支事务进入阻塞阶段,收到 XA commit 或 XA rollback 前必须阻塞等待。

事务资源长时间得不到释放,锁定周期长,而且在应用层上面无法干预,性能差。

适用场景

适用于想要迁移到 Seata 平台基于 XA 协议的老应用,使用 XA 模式将更平滑,还有 AT 模式未适配的数据库应用。

整体机制

  • 执行阶段:

    • 可回滚:业务 SQL 操作放在 XA 分支中进行,由资源对 XA 协议的支持来保证可回滚

    • 持久化:XA 分支完成后,执行 XA prepare,同样,由资源对 XA 协议的支持来保证持久化

  • 完成阶段:

    • 分支提交:执行 XA 分支的 commit

    • 分支回滚:执行 XA 分支的 rollback

模式总结

  • TCC是AT的扩展,允许自定义提交和回滚操作,所以不要求数据库支持ACID事务。

  • XA是阻塞等待所有分支事务都执行成功后提交事务,或者在任意分支事务执行失败后回滚。

AT模式基于支持本地ACID事务关系型数据库

  • 一阶段 prepare 行为:在本地事务中,一并提交业务数据更新和相应回滚日志记录。

  • 二阶段 commit 行为:马上成功结束,自动 异步批量清理回滚日志。

  • 二阶段 rollback 行为:通过回滚日志,自动 生成补偿操作,完成数据回滚。

相应的,TCC模式,不依赖于底层数据资源的事务支持:

  • 一阶段 prepare 行为:调用 自定义 的 prepare 逻辑。

  • 二阶段 commit 行为:调用 自定义 的 commit 逻辑。

  • 二阶段 rollback 行为:调用 自定义 的 rollback 逻辑。

所谓 TCC 模式,是指支持把自定义的分支事务纳入到全局事务的管理中。

使用Seata

启动Seata服务器

1、下载软件包

访问Seata-Server版本历史页面,下载当前最新稳定版本2.5.0

解压下载下来的压缩包,并将seata-server文件夹复制到D:/program目录下。

2、修改软件配置

参考官方文档:

https://seata.apache.org/zh-cn/docs/user/registry/nacos
https://seata.apache.org/zh-cn/docs/user/configuration/nacos

修改Seata-Sever的配置文件

D:/program/seata-server/conf/application.yml

只需要修改后半部分内容:namespace根据自己的实际情况修改。

seata:
  config:
    type: nacos
    nacos:
      server-addr: localhost:8848
      namespace: 030a5699-7f2f-4107-92a9-752655bda84e
      group: SEATA_GROUP
      data-id: seataServer.properties
  registry:
    type: nacos
    nacos:
      application: seata-server
      server-addr: localhost:8848
      group: SEATA_GROUP
      cluster: default
      namespace: 030a5699-7f2f-4107-92a9-752655bda84e
  store:
    mode: file

3、上传配置文件

将以下位置的文件添加到Nacos控制台。

D:\program\seata-server\script\config-center\config.txt
  • 命名空间:030a5699-7f2f-4107-92a9-752655bda84e

  • Group ID:SEATA_GROUP

  • Data ID:seataServer.properties

下面是修改完之后的正确内容(删除了无效和重复的配置)。

#For details about configuration items, see https://seata.apache.org/zh-cn/docs/user/configurations
#Transport configuration, for client and server
transport.protocol=seata
transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableTmClientBatchSendRequest=false
transport.enableRmClientBatchSendRequest=true
transport.enableTcServerBatchSendResponse=false
transport.rpcRmRequestTimeout=30000
transport.rpcTmRequestTimeout=30000
transport.rpcTcRequestTimeout=30000
transport.enableClientSharedEventLoopGroup=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
transport.serialization=seata
transport.compressor=none
#HTTP thread pool
transport.minHttpPoolSize=10
transport.maxHttpPoolSize=100
transport.maxHttpTaskQueueSize=1000
transport.httpPoolKeepAliveTime=500

transport.serverSocketSendBufSize=153600
transport.serverSocketResvBufSize=153600
transport.writeBufferHighWaterMark=67108864
transport.writeBufferLowWaterMark=1048576

transport.soBackLogSize=1024
transport.serverChannelMaxIdleTimeSeconds=30

transport.minServerPoolSize=50
transport.maxServerPoolSize=500
transport.maxTaskQueueSize=20000
transport.keepAliveTime=500


#Transaction routing rules configuration, only for the client
service.vgroupMapping.default_tx_group=default
#If you use a registry, you can ignore it
service.default.grouplist=127.0.0.1:8091
service.disableGlobalTransaction=false

client.metadataMaxAgeMs=30000
#Transaction rule configuration, only for the client
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=true
client.rm.tableMetaCheckerInterval=60000
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
client.rm.sagaJsonParser=fastjson
client.rm.tccActionInterceptorOrder=-2147482648
client.rm.sqlParserType=druid
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.defaultGlobalTransactionTimeout=60000
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
client.tm.interceptorOrder=-2147482648
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
#For TCC transaction mode
tcc.fence.logTableName=tcc_fence_log
tcc.fence.cleanPeriod=1h
# You can choose from the following options: fastjson, jackson, gson
tcc.contextJsonParserType=fastjson

#Log rule configuration, for client and server
log.exceptionRate=100

#Transaction storage configuration, only for the server. The file, db, and redis configuration values are optional.
store.mode=db
store.lock.mode=db
store.session.mode=db
#Used for password encryption
store.publicKey=

#These configurations are required if the `store mode` is `db`. If `store.mode,store.lock.mode,store.session.mode` are not equal to `db`, you can remove the configuration block.
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.cj.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true
store.db.user=root
store.db.password=root
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.distributedLockTable=distributed_lock
store.db.vgroupTable=vgroup_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000

store.db.druid.timeBetweenEvictionRunsMillis=120000
store.db.druid.minEvictableIdleTimeMillis=300000
store.db.druid.testWhileIdle=true
store.db.druid.testOnBorrow=false
store.db.druid.keepAlive=false

#Transaction rule configuration, only for the server
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.endstatusRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackFailedUnlockEnable=false
server.distributedLockExpireTime=10000
server.session.branchAsyncQueueSize=5000
server.session.enableBranchAsyncRemove=false
server.enableParallelRequestHandle=true
server.enableParallelRequestHandle=true
server.retryDeadThreshold=70000
server.applicationDataLimit=64000
server.applicationDataLimitCheck=false

server.ratelimit.enable=false
server.ratelimit.bucketTokenNumPerSecond = 999999
server.ratelimit.bucketTokenMaxNum = 999999
server.ratelimit.bucketTokenInitialNum = 999999

#Metrics configuration, only for the server
metrics.enabled=true
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898

4、运行启动脚本

双击运行Seata Server安装目录的bin目录下的脚本文件。

如图是启动成功的日志。

5、创建Seata数据库

创建用于存储Seata数据的MySQL数据库。

create database `seata` CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_general_ci';

在数据库下执行/script/server/db目录下的对应数据库类型SQL脚本文件。

6、创建UNDO_LOG表

在所有客户端(集成了Seata)应用的数据库中,执行对应版本的创建undo_log表的数据库脚本。

https://github.com/apache/incubator-seata/blob/2.x/script/client/at/db/mysql.sql

脚本内容如下:

-- 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`);

创建微服务应用

使用Maven项目聚合,将下面4个微服务作为模块放到一个Maven项目seata-study中。

  • 订单服务:order-service

  • 库存服务:stock-service

  • 账户服务:account-service

  • 业务服务器:business-server

前3个微服务作为服务提供方,业务服务器作为服务的消费方,通过RPC框架Dubbo实现不同服务间的调用。

创建父项目

在Intellij IDEA中创建一个Maven项目,由于项目是用于学习使用的,将项目取名为seata-study。

<?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>
    <packaging>pom</packaging>

    <version>20260101</version>
    <groupId>cn.edu.sgu.www</groupId>
    <artifactId>seata-study</artifactId>
    <description>Apache Seata分布式事务框架学习项目</description>

    <modules>
        <module>order-service</module>
        <module>stock-service</module>
        <module>account-service</module>
        <module>business-server</module>
    </modules>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.2.0</version>
            </plugin>
        </plugins>
    </build>
</project>

创建子模块

在seata-study项目下创建以下4个模块:

  • order-service

  • stock-service

  • account-service

  • business-server

seata-study项目的层次结构如下:

seata-study
	├── account-service
	|	├── account-service-interface
	|	├── account-service-provider
	|
	├── business-server
	|
	├── order-service
	|	├── order-service-interface
	|	├── order-service-provider
    |
	├── stock-service
		├── stock-service-interface
		├── stock-service-provider

库存服务(stock-service)
<?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>

    <packaging>pom</packaging>

    <parent>
        <artifactId>seata-study</artifactId>
        <groupId>cn.edu.sgu.www</groupId>
        <version>20260101</version>
    </parent>

    <artifactId>stock-service</artifactId>
    <description>库存服务</description>

    <modules>
        <module>stock-service-interface</module>
        <module>stock-service-provider</module>
    </modules>
</project>

库存服务下有两个子模块:stock-service-interface、stock-service-provider

stock-service-interface

stock-service-interface项目的目录结构如下。

pm.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>
        <artifactId>stock-service</artifactId>
        <groupId>cn.edu.sgu.www</groupId>
        <version>20260101</version>
    </parent>

    <artifactId>stock-service-interface</artifactId>

    <dependencies>
        <!--Lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.22</version>
            <optional>true</optional>
        </dependency>
    </dependencies>
</project>

Stock.java
package cn.edu.sgu.www.stock.entity;

import lombok.Data;

import java.io.Serializable;
import java.math.BigDecimal;

/**
 * @author 沐雨橙风ιε
 * @version 1.0
 */
@Data
public class Stock implements Serializable {
    private static final long serialVersionUID = 18L;

    private Integer id;

    /**
     * 剩余库存
     */
    private Integer count;

    /**
     * 商品ID
     */
    private String productId;

    /**
     * 出售价格
     */
    private BigDecimal price;
}

StockService.java

cn.edu.sgu.www.stock.service.StockService接口。

package cn.edu.sgu.www.stock.service;

import cn.edu.sgu.www.stock.entity.Stock;

/**
 * @author 沐雨橙风ιε
 * @version 1.0
 */
public interface StockService {

    /**
     * 扣减库存数量
     * @param productId 商品ID
     * @param count 扣减数量
     */
    void deduct(String productId, Integer count);

    /**
     * 查询商品库存信息
     * @param productId 商品ID
     */
    Stock selectByProductId(String productId);
}

stock-service-provider

stock-service-provider项目的目录结构如下。

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>
        <version>20260101</version>
        <groupId>cn.edu.sgu.www</groupId>
        <artifactId>service-parent</artifactId>
    </parent>

    <artifactId>stock-service-provider</artifactId>

    <dependencies>
        <dependency>
            <version>20260101</version>
            <groupId>cn.edu.sgu.www</groupId>
            <artifactId>stock-service-interface</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--Druid数据库连接池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
        </dependency>

        <!--Lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <!--MySQL驱动包-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!--Mybatis-Plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>

        <!-- Apache Dubbo -->
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-spring-boot-starter</artifactId>
        </dependency>

        <!-- Apache Seata -->
        <dependency>
            <groupId>org.apache.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
        </dependency>

        <!--Nacos注册中心-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

application.yml

包含了Dubbo、Seata、Nacos和MySQL的配置。

server:
  port: 8114

logging:
  level:
    cn.edu.sgu.www.stock: debug

dubbo:
  application:
    name: ${spring.application.name}
    logger: slf4j
    qos-enable: false
  consumer:
    timeout: 5000  # 远程调用的超时时间
    check: false # 关闭启动检查
  protocol:
    name: dubbo
    port: 20883
  registry:
    address: nacos://${spring.cloud.nacos.discovery.server-addr}  # 使用Nacos注册中心
    parameters:
      group: dubbo
  scan:
    base-packages: cn.edu.sgu.www.stock.service

seata:
  config:
    type: nacos
    nacos:
      group: SEATA_GROUP
      server-addr: localhost:8848
      namespace: 030a5699-7f2f-4107-92a9-752655bda84e
  registry:
    type: nacos
    nacos:
      group: SEATA_GROUP
      application: seata-server
      server-addr: localhost:8848
      namespace: 030a5699-7f2f-4107-92a9-752655bda84e
  service:
    vgroup-mapping:
      default_tx_group: default
  tx-service-group: default_tx_group

spring:
  application:
    name: stock-service
  cloud:
    nacos:
      discovery:
        group: DEFAULT_GROUP
        namespace: 030a5699-7f2f-4107-92a9-752655bda84e
        server-addr: localhost:8848
  datasource:
    username: root
    password: root
    url: jdbc:mysql://localhost:3306/seata-study
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource

StockMapper.java
package cn.edu.sgu.www.stock.mapper;

import cn.edu.sgu.www.stock.entity.Stock;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;

/**
 * @author 沐雨橙风ιε
 * @version 1.0
 */
@Mapper
public interface StockMapper extends BaseMapper<Stock> {

}

StockServiceImpl.java

使用@DubboService暴露一个StockService服务。

package cn.edu.sgu.www.stock.service;

import cn.edu.sgu.www.stock.entity.Stock;
import cn.edu.sgu.www.stock.mapper.StockMapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import lombok.RequiredArgsConstructor;
import org.apache.dubbo.config.annotation.DubboService;

/**
 * @author 沐雨橙风ιε
 * @version 1.0
 */
@DubboService
@RequiredArgsConstructor
public class StockServiceImpl implements StockService {

    private final StockMapper stockMapper;

    @Override
    public void deduct(String productId, Integer count) {
        UpdateWrapper<Stock> updateWrapper = new UpdateWrapper<>();

        updateWrapper.eq("product_id", productId);

        // 查询剩余库存
        Stock stock = stockMapper.selectOne(updateWrapper);

        if (stock == null) {
            throw new RuntimeException("没有找到商品的库存信息!");
        }

        Integer stockCount = stock.getCount();

        if (stockCount < count) { // 库存不足:中断操作,抛出异常
            throw new RuntimeException("商品库存不足!");
        }

        updateWrapper.ge("count", count);

        updateWrapper.set("count", stockCount - count);

        stockMapper.update(updateWrapper);
    }

    @Override
    public Stock selectByProductId(String productId) {
        QueryWrapper<Stock> queryWrapper = new QueryWrapper<>();

        queryWrapper.eq("product_id", productId);

        return stockMapper.selectOne(queryWrapper);
    }

}

账户服务(account-service)
<?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>

    <packaging>pom</packaging>

    <parent>
        <artifactId>seata-study</artifactId>
        <groupId>cn.edu.sgu.www</groupId>
        <version>20260101</version>
    </parent>

    <artifactId>account-service</artifactId>
    <description>账户服务</description>

    <modules>
        <module>account-service-interface</module>
        <module>account-service-provider</module>
    </modules>
</project>

账户服务下有两个子模块:account-service-interface、account-service-provider

account-service-interface

account-service-interface项目的目录结构如下。

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>
        <artifactId>account-service</artifactId>
        <groupId>cn.edu.sgu.www</groupId>
        <version>20260101</version>
    </parent>

    <artifactId>account-service-interface</artifactId>

    <dependencies>
        <!--Lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.22</version>
            <optional>true</optional>
        </dependency>
    </dependencies>
</project>

Account.java
package cn.edu.sgu.www.account.entity;

import lombok.Data;

import java.io.Serializable;
import java.math.BigDecimal;

/**
 * @author 沐雨橙风ιε
 * @version 1.0
 */
@Data
public class Account implements Serializable {
    private static final long serialVersionUID = 18L;

    private Integer id;

    /**
     * 账户名称
     */
    private String name;

    /**
     * 用户ID
     */
    private String userId;

    /**
     * 账户余额
     */
    private BigDecimal money;
}

AccountService.java
package cn.edu.sgu.www.account.service;

import java.math.BigDecimal;

/**
 * @author 沐雨橙风ιε
 * @version 1.0
 */
public interface AccountService {

    /**
     * 扣减用户账户的余额
     * @param userId 用户ID
     * @param money 扣减金额
     */
    void debit(String userId, BigDecimal money);
}

account-service-provider

account-service-provider项目的目录结构如下。

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>
        <version>20260101</version>
        <groupId>cn.edu.sgu.www</groupId>
        <artifactId>service-parent</artifactId>
    </parent>

    <artifactId>account-service-provider</artifactId>

    <dependencies>
        <dependency>
            <version>20260101</version>
            <groupId>cn.edu.sgu.www</groupId>
            <artifactId>account-service-interface</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--Druid数据库连接池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
        </dependency>

        <!--Lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <!--MySQL驱动包-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!--Mybatis-Plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>

        <!-- Apache Dubbo -->
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-spring-boot-starter</artifactId>
        </dependency>

        <!-- Apache Seata -->
        <dependency>
            <groupId>org.apache.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
        </dependency>

        <!--Nacos注册中心-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

application.yml
server:
  port: 8111

logging:
  level:
    cn.edu.sgu.www.account: debug

dubbo:
  application:
    name: ${spring.application.name}
    logger: slf4j
    qos-enable: false
  consumer:
    timeout: 5000  # 远程调用的超时时间
    check: false # 关闭启动检查
  protocol:
    name: dubbo
    port: 20881
  registry:
    address: nacos://${spring.cloud.nacos.discovery.server-addr}  # 使用Nacos注册中心
    parameters:
      group: dubbo
  scan:
    base-packages: cn.edu.sgu.www.account.service

seata:
  config:
    type: nacos
    nacos:
      group: SEATA_GROUP
      server-addr: localhost:8848
      namespace: 030a5699-7f2f-4107-92a9-752655bda84e
  registry:
    type: nacos
    nacos:
      application: seata-server
      group: SEATA_GROUP
      server-addr: localhost:8848
      namespace: 030a5699-7f2f-4107-92a9-752655bda84e
  service:
    vgroup-mapping:
      default_tx_group: default
  tx-service-group: default_tx_group

spring:
  application:
    name: account-service
  cloud:
    nacos:
      discovery:
        group: DEFAULT_GROUP
        namespace: 030a5699-7f2f-4107-92a9-752655bda84e
        server-addr: localhost:8848
  datasource:
    username: root
    password: root
    url: jdbc:mysql://localhost:3306/seata-study
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource

AccountMapper.java
package cn.edu.sgu.www.account.mapper;

import cn.edu.sgu.www.account.entity.Account;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;

/**
 * @author 沐雨橙风ιε
 * @version 1.0
 */
@Mapper
public interface AccountMapper extends BaseMapper<Account> {

}

AccountServiceImpl.java

使用@DubboService暴露一个AccountService服务。

package cn.edu.sgu.www.account.service;

import cn.edu.sgu.www.account.entity.Account;
import cn.edu.sgu.www.account.mapper.AccountMapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import lombok.RequiredArgsConstructor;
import org.apache.dubbo.config.annotation.DubboService;

import java.math.BigDecimal;

/**
 * @author 沐雨橙风ιε
 * @version 1.0
 */
@DubboService
@RequiredArgsConstructor
public class AccountServiceImpl implements AccountService {

    private final AccountMapper accountMapper;

    @Override
    public void debit(String userId, BigDecimal money) {
        UpdateWrapper<Account> updateWrapper = new UpdateWrapper<>();

        updateWrapper.eq("user_id", userId);

        // 查询用户的账户
        Account account = accountMapper.selectOne(updateWrapper);

        if (account == null) {
            throw new RuntimeException("您还未绑定账户!");
        }

        BigDecimal accountMoney = account.getMoney();

        if (accountMoney.compareTo(money) < 0) { // 账户余额不足:中断操作,抛出异常
            throw new RuntimeException("账户余额不足!");
        }

        updateWrapper.ge("money", money);
        updateWrapper.set("money", accountMoney.subtract(money));

        accountMapper.update(updateWrapper);
    }

}

订单服务(order-service)
<?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>

    <packaging>pom</packaging>

    <parent>
        <artifactId>seata-study</artifactId>
        <groupId>cn.edu.sgu.www</groupId>
        <version>20260101</version>
    </parent>

    <artifactId>order-service</artifactId>
    <description>订单服务</description>

    <modules>
        <module>order-service-interface</module>
        <module>order-service-provider</module>
    </modules>
</project>

订单服务下有两个子模块:order-service-interface、order-service-provider

order-service-interface

order-service-interface项目的目录结构如下。

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>
        <artifactId>order-service</artifactId>
        <groupId>cn.edu.sgu.www</groupId>
        <version>20260101</version>
    </parent>

    <artifactId>order-service-interface</artifactId>

    <dependencies>
        <!--Lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.22</version>
            <optional>true</optional>
        </dependency>

        <!-- Mybatis-Plus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus</artifactId>
            <version>3.5.12</version>
            <optional>true</optional>
        </dependency>
    </dependencies>
</project>

Order.java

由于order和MySQL关键字重名,使用表名`order`,同时主键通过自定义ID生成器生成。

package cn.edu.sgu.www.order.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.io.Serializable;
import java.math.BigDecimal;

/**
 * @author 沐雨橙风ιε
 * @version 1.0
 */
@Data
@TableName("`order`")
public class Order implements Serializable {
    private static final long serialVersionUID = 18L;

    @TableId(type = IdType.ASSIGN_UUID)
    private String id;

    /**
     * 用户ID
     */
    private String userId;

    /**
     * 商品ID
     */
    private String productId;

    /**
     * 订单数量
     */
    private Integer orderCount;

    /**
     * 订单金额
     */
    private BigDecimal orderMoney;
}

OrderService.java
package cn.edu.sgu.www.order.service;

/**
 * @author 沐雨橙风ιε
 * @version 1.0
 */
public interface OrderService {

    /**
     * 创建订单
     * @param userId 用户ID
     * @param productId 商品ID
     * @param orderCount 订单数量
     */
    void create(String userId, String productId, Integer orderCount);
}

order-service-provider

order-service-provider项目的目录结构如下。

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>
        <version>20260101</version>
        <groupId>cn.edu.sgu.www</groupId>
        <artifactId>service-parent</artifactId>
    </parent>

    <artifactId>order-service-provider</artifactId>

    <dependencies>
        <dependency>
            <version>20260101</version>
            <groupId>cn.edu.sgu.www</groupId>
            <artifactId>order-service-interface</artifactId>
        </dependency>

        <dependency>
            <version>20260101</version>
            <groupId>cn.edu.sgu.www</groupId>
            <artifactId>stock-service-interface</artifactId>
        </dependency>

        <dependency>
            <version>20260101</version>
            <groupId>cn.edu.sgu.www</groupId>
            <artifactId>account-service-interface</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--Druid数据库连接池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
        </dependency>

        <!--Lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <!--MySQL驱动包-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!--Mybatis-Plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>

        <!-- Apache Dubbo -->
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-spring-boot-starter</artifactId>
        </dependency>

        <!-- Apache Seata -->
        <dependency>
            <groupId>org.apache.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
        </dependency>

        <!--Nacos注册中心-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
    </dependencies>
</project>

application.yml
server:
  port: 8112

logging:
  level:
    cn.edu.sgu.www.order: debug

dubbo:
  application:
    name: ${spring.application.name}
    logger: slf4j
    qos-enable: false
  consumer:
    timeout: 5000  # 远程调用的超时时间
    check: false # 关闭启动检查
  protocol:
    name: dubbo
    port: 20882
  registry:
    address: nacos://${spring.cloud.nacos.discovery.server-addr}  # 使用Nacos注册中心
    parameters:
      group: dubbo
  scan:
    base-packages: cn.edu.sgu.www.order.service

seata:
  config:
    type: nacos
    nacos:
      group: SEATA_GROUP
      server-addr: localhost:8848
      namespace: 030a5699-7f2f-4107-92a9-752655bda84e
  registry:
    type: nacos
    nacos:
      group: SEATA_GROUP
      application: seata-server
      server-addr: localhost:8848
      namespace: 030a5699-7f2f-4107-92a9-752655bda84e
  service:
    vgroup-mapping:
      default_tx_group: default
  tx-service-group: default_tx_group

spring:
  application:
    name: order-service
  cloud:
    nacos:
      discovery:
        group: DEFAULT_GROUP
        namespace: 030a5699-7f2f-4107-92a9-752655bda84e
        server-addr: localhost:8848
  datasource:
    username: root
    password: root
    url: jdbc:mysql://localhost:3306/seata-study
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource

IdGenerator.java

MyBatis-Plus的自定义ID生成器,使用当前时间生成的UUID作为主键值。

package cn.edu.sgu.www.order.support;

import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * Mybatis Plus自定义ID生成器
 * @author 沐雨橙风ιε
 * @version 1.0
 */
@Component
@RequiredArgsConstructor
public class IdGenerator implements IdentifierGenerator {

    /**
     * 生成分布式ID
     * 数据库实体类的主键字段上使用了@TableId(type = IdType.ASSIGN_ID)注解时生效
     * @param entity 实体类对象
     */
    @Override
    public Number nextId(Object entity) {
        return null;
    }

    /**
     * 生成分布式UUID
     * 数据库实体类的主键字段上使用了@TableId(type = IdType.ASSIGN_UUID)注解时生效
     * @param entity 实体类对象
     */
    @Override
    public String nextUUID(Object entity) {
        return uuid();
    }

    /**
     * 根据当前时间生成UUID
     */
    private String uuid() {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
        LocalDateTime localDate = LocalDateTime.now();

        return localDate.format(formatter);
    }

}

OrderMapper.java
package cn.edu.sgu.www.order.mapper;

import cn.edu.sgu.www.order.entity.Order;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;

/**
 * @author 沐雨橙风ιε
 * @version 1.0
 */
@Mapper
public interface OrderMapper extends BaseMapper<Order> {

}

OrderServiceImpl.java

通过库存服务获取商品价格,用于计算订单金额。

package cn.edu.sgu.www.order.service;

import cn.edu.sgu.www.account.service.AccountService;
import cn.edu.sgu.www.order.entity.Order;
import cn.edu.sgu.www.order.mapper.OrderMapper;
import cn.edu.sgu.www.stock.entity.Stock;
import cn.edu.sgu.www.stock.service.StockService;
import lombok.RequiredArgsConstructor;
import org.apache.dubbo.config.annotation.DubboReference;
import org.apache.dubbo.config.annotation.DubboService;

import java.math.BigDecimal;

/**
 * @author 沐雨橙风ιε
 * @version 1.0
 */
@DubboService
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {

    private final OrderMapper orderMapper;

    @DubboReference
    private AccountService accountService;

    @DubboReference
    private StockService stockService;

    @Override
    public void create(String userId, String productId, Integer orderCount) {
        BigDecimal orderMoney = calculate(productId, orderCount);

        accountService.debit(userId, orderMoney);

        Order order = new Order();

        order.setUserId(userId);
        order.setProductId(productId);
        order.setOrderCount(orderCount);
        order.setOrderMoney(orderMoney);

        orderMapper.insert(order);
    }

    /**
     * 计算商品总金额
     * @param productId 商品ID
     * @param orderCount 订单数量
     */
    private BigDecimal calculate(String productId, int orderCount) {
        Stock stock = stockService.selectByProductId(productId);

        if (stock == null) {
            throw new RuntimeException("未找到该商品的库存信息!");
        }

        return stock.getPrice().multiply(
                BigDecimal.valueOf(orderCount)
        );
    }

}

业务服务器(business-server)

business-server项目的目录结构如下。

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>
        <version>20260101</version>
        <groupId>cn.edu.sgu.www</groupId>
        <artifactId>service-parent</artifactId>
    </parent>

    <artifactId>business-server</artifactId>

    <dependencies>
        <dependency>
            <version>20260101</version>
            <groupId>cn.edu.sgu.www</groupId>
            <artifactId>order-service-interface</artifactId>
        </dependency>

        <dependency>
            <version>20260101</version>
            <groupId>cn.edu.sgu.www</groupId>
            <artifactId>stock-service-interface</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--Druid数据库连接池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
        </dependency>

        <!--Lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <!--MySQL驱动包-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!--Mybatis-Plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>

        <!-- Apache Dubbo -->
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-spring-boot-starter</artifactId>
        </dependency>

        <!-- Apache Seata -->
        <dependency>
            <groupId>org.apache.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
        </dependency>

        <!--Nacos注册中心-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

application.yml
server:
  port: 8115

dubbo:
  application:
    name: ${spring.application.name}
    logger: slf4j
    qos-enable: false
  consumer:
    timeout: 5000  # 远程调用的超时时间
    check: false # 关闭启动检查
  protocol:
    name: tri
    port: 50051
  registry:
    address: nacos://${spring.cloud.nacos.discovery.server-addr}?group=dubbo # 使用Nacos注册中心

seata:
  config:
    type: nacos
    nacos:
      group: SEATA_GROUP
      server-addr: localhost:8848
      namespace: 030a5699-7f2f-4107-92a9-752655bda84e
  registry:
    type: nacos
    nacos:
      group: SEATA_GROUP
      application: seata-server
      server-addr: localhost:8848
      namespace: 030a5699-7f2f-4107-92a9-752655bda84e
  service:
    vgroup-mapping:
      default_tx_group: default
  tx-service-group: default_tx_group

spring:
  application:
    name: business-server
  cloud:
    nacos:
      discovery:
        group: DEFAULT_GROUP
        namespace: 030a5699-7f2f-4107-92a9-752655bda84e
        server-addr: localhost:8848
  datasource:
    username: root
    password: root
    url: jdbc:mysql://localhost:3306/seata-study
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource

BusinessService.java
package cn.edu.sgu.www.business.service;

/**
 * @author 沐雨橙风ιε
 * @version 1.0
 */
public interface BusinessService {

    /**
     * 购买商品
     * @param userId 用户ID
     * @param productId 商品ID
     * @param orderCount 扣减数量
     */
    void purchase(String userId, String productId, Integer orderCount);
}

BusinessController.java
package cn.edu.sgu.www.business.controller;

import cn.edu.sgu.www.business.service.BusinessService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author 沐雨橙风ιε
 * @version 1.0
 */
@RestController
@RequiredArgsConstructor
public class BusinessController {

    private final BusinessService businessService;

    @PostMapping("/purchase")
    public void purchase(String userId, String productId, Integer orderCount) {
        businessService.purchase(userId, productId, orderCount);
    }

}

BusinessServiceImpl.java

最终的业务方法,使用Seata的@GlobalTransactional注解开启全局事务。

package cn.edu.sgu.www.business.service.impl;

import cn.edu.sgu.www.business.service.BusinessService;
import cn.edu.sgu.www.order.service.OrderService;
import cn.edu.sgu.www.stock.service.StockService;
import org.apache.dubbo.config.annotation.DubboReference;
import org.apache.seata.spring.annotation.GlobalTransactional;
import org.springframework.stereotype.Service;

/**
 * @author 沐雨橙风ιε
 * @version 1.0
 */
@Service
public class BusinessServiceImpl implements BusinessService {

    @DubboReference
    private StockService stockService;

    @DubboReference
    private OrderService orderService;

    @GlobalTransactional
    @Override
    public void purchase(String userId, String productId, Integer orderCount) {
        stockService.deduct(productId, orderCount);

        orderService.create(userId, productId, orderCount);
    }

}

启动微服务应用

依次启动四个微服务,待所有服务启动完毕,通过Postman发送POST请求到/purcharse接口。

成功的事务

账户成功扣减金额15

商品ID为1的库存扣减5

创建了一个金额15元的订单

失败的事务

在订单微服务的创建订单方法最后面主动制造一个除零异常,模拟事务失败的场景。

重新启动订单服务,再次发送相同的请求(在操作之前清空四个程序的控制台日志)。

这次请求返回了500状态码,然后看一下控制台日志和数据库的数据,确认是否执行了回滚操作。

业务服务器打印了调用订单服务的创建订单的方法抛出的异常栈信息。

账户服务的分支事务回滚成功。

库存服务的分支事务回滚成功。

异常之后,订单服务的分支事务也已经回滚了。

检查数据库的数据:

  • 账户余额回滚了:70 => 85
  • 创建的订单已经被删除
  • 库存从恢复了:0 => 5

至此,Seata的分布式事务已经能够正确地执行了。


文章的代码已经上传到Git,需要的自行下载

Apache Seata学习项目https://gitee.com/muyu-chengfeng/seata-study.git

父项目地址:定义了和Spring Boot 2.3.4.RELEASE版本兼容的各种工具依赖的版本。

沐雨橙风ιε的项目中使用的公共父模块 https://gitee.com/muyu-chengfeng/service-parent.git

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

沐雨橙风ιε

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值