【微服务 32】你为Spring Cloud整合Seata、Nacos实现分布式事务案例跑不起来苦恼过吗(巨细排坑版)

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
简介: 【微服务 32】你为Spring Cloud整合Seata、Nacos实现分布式事务案例跑不起来苦恼过吗(巨细排坑版)

@[TOC]

一、前言

至此,微服务系列正式开启分布式事务篇;

捎带一提,seata官方给的案例是真的******,版本之间的差异并未说明,据悉官方案例属于政治任务!在开启案例之前,博主和网友们踩过一些坑,具体见文章:

  1. can not get cluster name in registry config ‘service.vgroupMapping.xx‘, please make sure registry问题解决
  2. Seata Failed to get available servers: endpoint format should like ip:port 报错原因/解决方案汇总版(看完本文必解决问题)
  3. Seata json decode exception, Cannot construct instance of java.time.LocalDateTime报错原因/解决方案最全汇总版
  4. 【微服务 31】超细的Spring Cloud 整合Seata实现分布式事务(排坑版)

本文基于AT模式 +搭建SpringCloud 集成 Seata + Nacos 实现分布式事务的案例;

版本信息如下:

<properties>
    <spring-boot.version>2.4.2</spring-boot.version>
    <spring-cloud.version>2020.0.1</spring-cloud.version>
    <spring-cloud-alibaba.version>2021.1</spring-cloud-alibaba.version>
    <mysql.version>8.0.22</mysql.version>
</properties>

二、Seata简介

在这里插入图片描述

Seata 是一款开源的分布式事务解决方案,全称:Simple extensiable autonomous transaction architecture;意思是:简单的、可扩展的、自治的事务架构。Seata致力于提供高性能和简单易用的分布式事务服务;Seata 为用户提供了 AT、TCC、SAGA 和 XA 四种分布式事务模式;

1> AT模式:

  • 提供无侵入自动补偿的事务模式,目前已支持MySQL、Oracle、PostgreSQL、TiDB 和 MariaDB;

2> TCC 模式:

  • 支持 TCC 模式并可与 AT 混用,灵活度更高;

3> SAGA 模式:

  • 为长事务提供有效的解决方案,提供编排式与注解式(开发中);

4> XA 模式:

  • 支持已实现 XA 接口的数据库的 XA 模式,目前已支持MySQL、Oracle、TiDB和MariaDB;

官方文档:[https://seata.io/zh-cn/docs/overview/what-is-seata.html
](https://seata.io/zh-cn/docs/overview/what-is-seata.html)

三个角色

1> TC (Transaction Coordinator) - 事务协调者

  • 维护全局和分支事务的状态,驱动全局事务提交或回滚。

2> TM (Transaction Manager) - 事务管理器

  • 定义全局事务的范围:开始全局事务、提交或回滚全局事务。

3> RM (Resource Manager) - 资源管理器

  • 管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

案例中三个角色的交互

在这里插入图片描述

三、SpringCloud 集成Seata(注册和配置均采用Nacos)

本文基于AT模式 +搭建SpringCloud 集成 Seata + Nacos 实现分布式事务的案例;

整体项目目录包括四个Module,分别为:trade-center、stock-service、order-service、account-service。
在这里插入图片描述

用例为用户购买商品的业务逻辑,整个业务逻辑由3个微服务提供支持,其中:

  • 仓储服务(stock-service):对给定的商品扣除仓储数量。
  • 订单服务(order-service):根据采购需求创建订单。
  • 帐户服务(account-service):从用户帐户中扣除余额。

此外,trade-center为交易中心,是处理用户请求的入口;

0、业务架构图

在这里插入图片描述

1、MySQL数据库信息

必须要使用具有InnoDB引擎的MySQL;也就是说数据库的引擎要支持事务,因为AT模式底层是依赖数据库事务实现的分布式事务。

在案例中,仓储服务(stock-service)、订单服务(order-service)、帐户服务(account-service) 这三个服务对应三个数据库,为了方便测试,我们只创建一个数据库并配置3个数据源。

0)一键执行所有SQL

1> 案例中seata-client相关的所有业务库、业务表、undo_log表创建SQL;
2> seata-server保存数据的表;

#Account
DROP SCHEMA IF EXISTS seata_account;
CREATE SCHEMA seata_account;
USE seata_account;

CREATE TABLE `account_tbl`
(
    `id`      INT(11) NOT NULL AUTO_INCREMENT,
    `user_id` VARCHAR(255) DEFAULT NULL,
    `money`   INT(11) DEFAULT 0,
    PRIMARY KEY (`id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

INSERT INTO account_tbl (id, user_id, money)
VALUES (1, '1001', 10000);
INSERT INTO account_tbl (id, user_id, money)
VALUES (2, '1002', 10000);

CREATE TABLE `undo_log`
(
    `id`            bigint(20) NOT NULL AUTO_INCREMENT,
    `branch_id`     bigint(20) NOT NULL,
    `xid`           varchar(100) NOT NULL,
    `context`       varchar(128) NOT NULL,
    `rollback_info` longblob     NOT NULL,
    `log_status`    int(11) NOT NULL,
    `log_created`   datetime     NOT NULL,
    `log_modified`  datetime     NOT NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

#Order
DROP SCHEMA IF EXISTS seata_order;
CREATE SCHEMA seata_order;
USE seata_order;

CREATE TABLE `order_tbl`
(
    `id`             INT(11) NOT NULL AUTO_INCREMENT,
    `user_id`        VARCHAR(255) DEFAULT NULL,
    `commodity_code` VARCHAR(255) DEFAULT NULL,
    `count`          INT(11) DEFAULT '0',
    `money`          INT(11) DEFAULT '0',
    PRIMARY KEY (`id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

CREATE TABLE `undo_log`
(
    `id`            bigint(20) NOT NULL AUTO_INCREMENT,
    `branch_id`     bigint(20) NOT NULL,
    `xid`           varchar(100) NOT NULL,
    `context`       varchar(128) NOT NULL,
    `rollback_info` longblob     NOT NULL,
    `log_status`    int(11) NOT NULL,
    `log_created`   datetime     NOT NULL,
    `log_modified`  datetime     NOT NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

#Stock
DROP SCHEMA IF EXISTS seata_stock;
CREATE SCHEMA seata_stock;
USE seata_stock;

CREATE TABLE `stock_tbl`
(
    `id`             INT(11) NOT NULL AUTO_INCREMENT,
    `commodity_code` VARCHAR(255) DEFAULT NULL,
    `count`          INT(11) DEFAULT '0',
    PRIMARY KEY (`id`),
    UNIQUE KEY `commodity_code` (`commodity_code`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;


INSERT INTO stock_tbl (id, commodity_code, count)
VALUES (1, '2001', 1000);

CREATE TABLE `undo_log`
(
    `id`            bigint(20) NOT NULL AUTO_INCREMENT,
    `branch_id`     bigint(20) NOT NULL,
    `xid`           varchar(100) NOT NULL,
    `context`       varchar(128) NOT NULL,
    `rollback_info` longblob     NOT NULL,
    `log_status`    int(11) NOT NULL,
    `log_created`   datetime     NOT NULL,
    `log_modified`  datetime     NOT NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

-- the table to store GlobalSession data
DROP SCHEMA IF EXISTS seata_server;
CREATE SCHEMA seata_server;
USE seata_server;

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_gmt_modified_status` (`gmt_modified`, `status`),
    KEY `idx_transaction_id` (`transaction_id`)
    ) ENGINE = InnoDB
    DEFAULT CHARSET = utf8;

-- 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 = utf8;

-- 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),
    `gmt_create`     DATETIME,
    `gmt_modified`   DATETIME,
    PRIMARY KEY (`row_key`),
    KEY `idx_branch_id` (`branch_id`)
    ) ENGINE = InnoDB
    DEFAULT CHARSET = utf8;

1)undo_log 事务回滚日志表

SEATA AT 模式需要 undo_log 表,用于事务回滚使用。所以上面三个服务每个服务都要有一个undo_log表。表结构如下:
在这里插入图片描述
建表SQL:

CREATE TABLE `undo_log`
(
    `id`            bigint(20) NOT NULL AUTO_INCREMENT,
    `branch_id`     bigint(20) NOT NULL,
    `xid`           varchar(100) NOT NULL,
    `context`       varchar(128) NOT NULL,
    `rollback_info` longblob     NOT NULL,
    `log_status`    int(11) NOT NULL,
    `log_created`   datetime     NOT NULL,
    `log_modified`  datetime     NOT NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

undo_log表的结构是从哪里找的?为什么它是这个?看了一些文章并没有说这个,本文简要说明一下;

undo_log表结构从哪里找?

1> 在GitHub中找到seata的源码,选择响应版本的代码分支:

在这里插入图片描述

注意源码最上层目录结构下有一个script文件夹,其中记录了所有我们可能需要的SQL、配置....。比如:集成Nacos时,配置的内容、上传配置的shell脚本都在其中。

2> 进入目录/script/client/at/db,找到mysql.sql文件,其就是我们需要的创建undo_log表结构的SQL:

在这里插入图片描述

2)仓储服务(stock-service)业务表

1> 表结构:

在这里插入图片描述

2> 建表SQL:

DROP SCHEMA IF EXISTS seata_stock;
CREATE SCHEMA seata_stock;
USE seata_stock;

CREATE TABLE `stock_tbl`
(
    `id`             INT(11) NOT NULL AUTO_INCREMENT,
    `commodity_code` VARCHAR(255) DEFAULT NULL,
    `count`          INT(11) DEFAULT '0',
    PRIMARY KEY (`id`),
    UNIQUE KEY `commodity_code` (`commodity_code`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

INSERT INTO stock_tbl (id, commodity_code, count)
VALUES (1, '2001', 1000);

3)订单服务(order-service)业务表

1> 表结构:

在这里插入图片描述

2> 建表SQL:

DROP SCHEMA IF EXISTS seata_order;
CREATE SCHEMA seata_order;
USE seata_order;

CREATE TABLE `order_tbl`
(
    `id`             INT(11) NOT NULL AUTO_INCREMENT,
    `user_id`        VARCHAR(255) DEFAULT NULL,
    `commodity_code` VARCHAR(255) DEFAULT NULL,
    `count`          INT(11) DEFAULT '0',
    `money`          INT(11) DEFAULT '0',
    PRIMARY KEY (`id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

4)账户服务(account)业务表

1> 表结构:

在这里插入图片描述

2> 建表SQL:

DROP SCHEMA IF EXISTS seata_account;
CREATE SCHEMA seata_account;
USE seata_account;

CREATE TABLE `account_tbl`
(
    `id`      INT(11) NOT NULL AUTO_INCREMENT,
    `user_id` VARCHAR(255) DEFAULT NULL,
    `money`   INT(11) DEFAULT 0,
    PRIMARY KEY (`id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

INSERT INTO account_tbl (id, user_id, money)
VALUES (1, '1001', 10000);
INSERT INTO account_tbl (id, user_id, money)
VALUES (2, '1002', 10000);

5)seata-server表结构

当seata-server配置信息中 store配置的是db时,
在这里插入图片描述

需要使用到三张表:global_table(记录全局事务)、branch_table(记录分支事务)、lock_table(记录全局锁);

当然数据库表和表名是可以改变的,只需要在store配置中对应上即可。

1> global_table

1> 表结构:

在这里插入图片描述

2> 建表SQL:

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_gmt_modified_status` (`gmt_modified`, `status`),
    KEY `idx_transaction_id` (`transaction_id`)
    ) ENGINE = InnoDB
    DEFAULT CHARSET = utf8;

2> branch_table

1> 表结构:

在这里插入图片描述

2> 建表SQL:

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 = utf8;

3> lock_table

1> 表结构:

在这里插入图片描述

2> 建表SQL:

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),
    `gmt_create`     DATETIME,
    `gmt_modified`   DATETIME,
    PRIMARY KEY (`row_key`),
    KEY `idx_branch_id` (`branch_id`)
    ) ENGINE = InnoDB
    DEFAULT CHARSET = utf8;

seata-server相关表结构从哪里找?

1> 在GitHub中找到seata的源码,选择响应版本的代码分支:

在这里插入图片描述

注意源码最上层目录结构下有一个script文件夹,其中记录了所有我们可能需要的SQL、配置....。比如:集成Nacos时,配置的内容、上传配置的shell脚本都在其中。

2> 进入目录/script/server/db,找到mysql.sql文件,其就是我们需要的创建seata-server相关的表结构的SQL:

在这里插入图片描述

数据库表结构处理完之后,看一下seata-server需要如何下载、配置、启动?

2、seata-server

seata-server下载地址:https://seata.io/zh-cn/blog/download.html,其中binary选项为seata-server可执行程序,source为相应版本源码。

1)seata-server配置

将下载下来的seata-server-1.3.0.tar.gz压缩包解压,解压后的文件目录为:seata-server-1.3.0

# 进入seata-server主目录
cd seata-server-1.3.0
# 进入seata-server配置目录
cd conf

修改registry.conffile.conf配置文件,内容如下:

1> registry.conf

seata-server的配置中心和注册中心均采用nacos的方式,

设置type为nacos、设置serverAddr为nacos节点地址,serverAddr不能带‘http://’前缀

特别注意:registry.nacos.cluster(注册到的nacos集群名称)要和事务分组名称一致,即:下面config.txt文件中service.vgroupMapping.saint-trade-tx-group对应的值seata-server-sh

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

  nacos {
    application = "seata-server"
    serverAddr = "127.0.0.1:8848"
    group = "SEATA_GROUP"
    namespace = ""
    cluster = "seata-server-sh"
  }
}

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

  nacos {
    serverAddr = "127.0.0.1:8848"
    namespace = ""
    group = "SEATA_GROUP"
  }
}

为什么要用service.vgroupMapping.saint-trade-tx-group对应的值作为seata-server注册到nacos注册中心的集群名?

  • ==因为采用nacos作为注册中心时,seata-client是通过服务名(seata-server) + 集群名(seata-server-sh)+ 分组(SEATA_GROUP)去nacos注册中心中找到所有的seata-server实例地址;==
    在这里插入图片描述
  • 如果不设置相应的集群名,则在从Nacos获取服务实例列表时会报错:

    i.s.c.r.netty.NettyClientChannelManager : no available service 'seata-server-sh' found, please make sure registry config correct
    在这里插入图片描述

这里涉及到Nacos的源码,感兴趣的可以看博主的另一篇文章:图文详述Nacos服务发现源码分析

2> seata-server相关配置上传到Nacos配置中心

方法一:
(1) 在GitHub中找到seata的源码,选择响应版本的代码分支:

在这里插入图片描述

注意源码最上层目录结构下有一个script文件夹,其中记录了所有我们可能需要的SQL、配置....。比如:集成Nacos时,配置的内容、上传配置的shell脚本都在其中。

(2)进入/script/config-center目录,其中config.txt是上传到配置中心的配置所在的文件,nacos目录中是将nacos上传到Nacos配置中心的脚本(nacos-config.shnacos-config.py):

在这里插入图片描述
在这里插入图片描述

(3)将上面的config.txt文件复制到seata-server的根目录,将nacos-config.sh复制到seat-server的bin目录下:
注意:config.txt文件必须要在nacos-config.sh的上一级目录(../

  • 因为在nacos-config.sh脚本中,是通过cat $(dirname "$PWD")的方式去查找config.txt所在是目录;而cat $(dirname "$PWD")返回的正是nacos-config.sh脚本所在目录的上级目录;
    在这里插入图片描述

(4)修改config.txt文件:

重点关注以下配置:
  • 事务分组名称的配置、事务分组名称对应的Seata Server服务实例列表;
    注意,高版本的vgroupMapping后面的如: saint-trade-tx-group 不能定义为 saint_trade_tx_group
  • Seata Server数据存放的模式,如果是模式db,配置db信息;

在这里插入图片描述

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
service.vgroupMapping.saint-trade-tx-group=seata-server-sh
service.seata-server-sh.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.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
store.mode=db
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_server?useUnicode=true
store.db.user=root
store.db.password=123456
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
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.log.exceptionRate=100
transport.serialization=seata
transport.compressor=none
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898

(5)执行nacos-config.sh脚本将配置上传到nacos配置中心:

方法二:通过dataId配置

  1. 从seata的v1.4.2版本开始,已支持从一个Nacos dataId中获取所有配置信息,只需要额外添加一个dataId配置项。
  2. 首先在nacos新建配置,此处dataId为seataServer.properties,配置内容参考https://github.com/seata/seata/tree/develop/script/config-center 的config.txt并按需修改(见方法一的修改)保存;
  3. 在client参考如下配置进行修改:

    seata:
      config:
        type: nacos
        nacos:
          server-addr: 127.0.0.1:8848
          group : "SEATA_GROUP"
          namespace: ""
          dataId: "seataServer.properties"
          username: "nacos"
          password: "nacos"

2)启动seata-server

进入seata-server-1.3.0/bin目录,然后运行seata-server.sh shell脚本;

cd ../bin
sh seata-server.sh

seata-server.sh脚本中的参数

具体参考官方文档:https://github.com/seata/seata/tree/develop/script/config-center

v1.5.0开始支持的 Nacos相关启动脚本参数介绍:

在这里插入图片描述

3、seata-client

一般Spring Cloud集成seata大致会分为5步:

1> 第一步:

  • 添加Spring Cloud Alibaba 依赖管理工具和 Seata 依赖;

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>2021.1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    </dependency>

另外,spring-cloud-starter-alibaba-seata依赖中seata相关的只依赖了spring-cloud-alibaba-seata,所以在项目中添加spring-cloud-starter-alibaba-seataspring-cloud-alibaba-seata是一样的;

2> 第二步:

  • 在application.yml配置文件中设置Seata相关配置,(不再需要file.conf文件,registry.conf也不需要)

3> 第三步:

  • 注入数据源;Seata 通过代理数据源的方式实现分支事务;MyBatis 和 JPA 都需要注入 io.seata.rm.datasource.DataSourceProxy, 不同的是,MyBatis 还需要额外注入 org.apache.ibatis.session.SqlSessionFactory

4> 第四步:

  • 在业务相关的数据库中添加 undo_log 表,用于保存需要回滚的数据;

5> 第五步:

  • 在业务的发起方的方法上使用@GlobalTransactional开启全局事务,Seata 会将事务的 xid 通过拦截器添加到调用其他服务的请求中,实现分布式事务;

上面提到整体项目目录包括四个Module,分别为:trade-center、stock-service、order-service、account-service。
在这里插入图片描述

下面从这四个Module以具体的代码来看,这五步是如何体现在代码中的;

0)最上层父项目spring-cloud-center的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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.2</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <modules>
        <module>trade-center</module>
        <module>stock-service</module>
        <module>order-service</module>
        <module>account-service</module>
    </modules>

    <groupId>com.saint</groupId>
    <artifactId>transaction-seata</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>transaction-seata</name>
    <description>transaction-seata</description>
    <packaging>pom</packaging>

    <properties>
        <java.version>1.8</java.version>
        <spring-boot.version>2.4.2</spring-boot.version>
        <spring-cloud.version>2020.0.1</spring-cloud.version>
        <spring-cloud-alibaba.version>2021.1</spring-cloud-alibaba.version>
        <druid.version>1.2.8</druid.version>
        <mysql.version>8.0.22</mysql.version>
    </properties>


    <dependencies>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.12.0</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>2.0.10</version>
        </dependency>
    </dependencies>

    <dependencyManagement>

        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!--整合spring cloud-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!--整合spring cloud alibaba-->
            <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>com.alibaba</groupId>
                <artifactId>druid-spring-boot-starter</artifactId>
                <version>${druid.version}</version>
            </dependency>

            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring-boot.version}</version>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

关于Spring-cloud和SpringBoot的版本对应关系,参考博文:SpringBoot、SpringCloud、SpringCloudAlibaba的版本对应关系

1)account-service

account-service整体代码结构目录如下:
在这里插入图片描述

整体包括:pom.xml、JPA数据源配置、一个Controller、一个entity、一个Dao、一个Service、一个启动类;application.yml配置文件;

1> 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>transaction-seata</artifactId>
        <groupId>com.saint</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <version>0.0.1-SNAPSHOT</version>
    <groupId>com.saint</groupId>
    <artifactId>account-service</artifactId>
    <name>account-service</name>

    <dependencies>

        <!--protostuff序列化依赖-->
        <dependency>
            <groupId>io.protostuff</groupId>
            <artifactId>protostuff-runtime</artifactId>
            <version>1.8.0</version>
        </dependency>
        <dependency>
            <groupId>io.protostuff</groupId>
            <artifactId>protostuff-core</artifactId>
            <version>1.8.0</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</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-seata</artifactId>
        </dependency>
    </dependencies>

</project>

2> DataSourceConfig

package com.saint.account.config;

import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import javax.sql.DataSource;

/**
 * 数据源配置
 *
 * @author Saint
 */
@Configuration
public class DataSourceConfig {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DruidDataSource druidDataSource() {
        return new DruidDataSource();
    }

    /**
     * 需要将 DataSourceProxy 设置为主数据源,否则事务无法回滚
     *
     * @param druidDataSource The DruidDataSource
     * @return The default datasource
     */
    @Primary
    @Bean("dataSource")
    public DataSource dataSource(DruidDataSource druidDataSource) {
        return new DataSourceProxy(druidDataSource);
    }
}

3> AccountController

package com.saint.account.controller;

import com.saint.account.service.AccountService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.math.BigDecimal;

/**
 * @author Saint
 */
@RestController
@RequiredArgsConstructor(onConstructor = @_(@Autowired))
public class AccountController {

    private final AccountService accountService;

    @RequestMapping("/debit")
    public Boolean debit(String userId, BigDecimal money) {
        accountService.debit(userId, money);

        return true;
    }
}

4> Account

package com.saint.account.entity;

import lombok.Data;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import java.math.BigDecimal;

/**
 * @author Saint
 */
@Entity
@Table(name = "account_tbl")
@DynamicUpdate
@DynamicInsert
@Data
public class Account {
    @Id
    private Long id;
    private String userId;
    private BigDecimal money;
}

5> AccountDAO

package com.saint.account.repository;

import com.saint.account.entity.Account;
import org.springframework.data.jpa.repository.JpaRepository;

/**
 * @author Saint
 */
public interface AccountDAO extends JpaRepository<Account, Long> {

    Account findByUserId(String userId);

}

6> AccountService

package com.saint.account.service;

import com.saint.account.entity.Account;
import com.saint.account.repository.AccountDAO;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;

/**
 * @author Saint
 */
@Service
@RequiredArgsConstructor(onConstructor = @_(@Autowired))
public class AccountService {

    private final AccountDAO accountDAO;

    private static final String ERROR_USER_ID = "1002";

    @Transactional(rollbackFor = Exception.class)
    public void debit(String userId, BigDecimal num) {
        Account account = accountDAO.findByUserId(userId);
        account.setMoney(account.getMoney().subtract(num));
        accountDAO.save(account);

        if (ERROR_USER_ID.equals(userId)) {
            throw new RuntimeException("account branch exception");
        }
    }
}

7> AccountApplication

package com.saint.account;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

/**
 * @author Saint
 */
@SpringBootApplication
@EnableFeignClients
@EnableJpaRepositories
public class AccountApplication {

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

8> application.yml

server:
  port: 9031
spring:
  application:
    name: account-service
  cloud:
    nacos:
      # 这里配置的是当前服务所要注册到的Nacos地址
      discovery:
        server-addr: 127.0.0.1:8848
        group: TRADE_GROUP
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/seata_account?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    show-sql: true

seata:
  # 属性需要seata-server端 config.txt文件中的service.vgroupMapping后的值保持一致
  # 即,nacos配置中心中要存在dataId为 service.vgroupMapping.saint-trade-tx-group 的配置
  tx-service-group: saint-trade-tx-group
  # 采用nacos作为配置中心
  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP
  # 当seata-server采用nacos做为注册中心时(通过以下信息获取seata-server实例列表)
  registry:
    type: nacos
    nacos:
      # seata-server的应用名称
      application: seata-server
      # seata-server注册到的nacos服务地址
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP

2)order-service

order-service整体代码结构目录如下:
在这里插入图片描述

整体包括:pom.xml、JPA数据源配置、一个Controller、一个entity、一个FeignClient、一个Dao、一个Service、一个启动类;application.yml配置文件;

1> 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>transaction-seata</artifactId>
        <groupId>com.saint</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <version>0.0.1-SNAPSHOT</version>
    <groupId>com.saint</groupId>
    <artifactId>order-service</artifactId>
    <name>order-service</name>

    <dependencies>

        <!--protostuff序列化依赖-->
        <dependency>
            <groupId>io.protostuff</groupId>
            <artifactId>protostuff-runtime</artifactId>
            <version>1.8.0</version>
        </dependency>
        <dependency>
            <groupId>io.protostuff</groupId>
            <artifactId>protostuff-core</artifactId>
            <version>1.8.0</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <!--负载均衡器-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-loadbalancer</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</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-seata</artifactId>
        </dependency>

    </dependencies>

</project>

2> DataSourceConfig

JPA的数据源配置和account-service中的一样;

3> OrderController

package com.saint.order.controller;

import com.saint.order.service.OrderService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author Saint
 */
@RestController
@RequiredArgsConstructor(onConstructor = @_(@Autowired))
public class OrderController {

    private final OrderService orderService;

    @GetMapping("/create")
    public Boolean create(String userId, String commodityCode, Integer count) {

        orderService.create(userId, commodityCode, count);
        return true;
    }

}

4> Order

package com.saint.order.entity;

import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;

import javax.persistence.*;
import java.math.BigDecimal;

/**
 * @author Saint
 */
@Entity
@Table(name = "order_tbl")
@DynamicUpdate
@DynamicInsert
@NoArgsConstructor
@Data
public class Order {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "user_id")
    private String userId;

    @Column(name = "commodity_code")
    private String commodityCode;

    @Column(name = "money")
    private BigDecimal money;

    @Column(name = "count")
    private Integer count;

}

5> AccountFeignClient

AccountFeignClient用于通过OpenFeign调用account-service;

package com.saint.order.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.math.BigDecimal;

/**
 * @author Saint
 */
@FeignClient(name = "account-service")
public interface AccountFeignClient {

    @GetMapping("/debit")
    Boolean debit(@RequestParam("userId") String userId, @RequestParam("money") BigDecimal money);
}

6> OrderDAO

package com.saint.order.repository;

import com.saint.order.entity.Order;
import org.springframework.data.jpa.repository.JpaRepository;

/**
 * @author Saint
 */
public interface OrderDAO extends JpaRepository<Order, Long> {

}

7> OrderService

package com.saint.order.service;

import com.saint.order.entity.Order;
import com.saint.order.feign.AccountFeignClient;
import com.saint.order.repository.OrderDAO;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;

/**
 * @author Saint
 */
@Service
@RequiredArgsConstructor(onConstructor = @_(@Autowired))
public class OrderService {

    private final AccountFeignClient accountFeignClient;
    private final OrderDAO orderDAO;

    @Transactional
    public void create(String userId, String commodityCode, Integer count) {

        BigDecimal orderMoney = new BigDecimal(count).multiply(new BigDecimal(5));
        Order order = new Order();
        order.setUserId(userId);
        order.setCommodityCode(commodityCode);
        order.setCount(count);
        order.setMoney(orderMoney);

        orderDAO.save(order);
        accountFeignClient.debit(userId, orderMoney);

    }

}

8> OrderApplication

package com.saint.order;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

/**
 * @author Saint
 */
@SpringBootApplication
@EnableFeignClients
@EnableJpaRepositories
public class OrderApplication {

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

}

9> application.yml

server:
  port: 9021
spring:
  application:
    name: order-service
  cloud:
    nacos:
      # 这里配置的是当前服务所要注册到的Nacos地址
      discovery:
        server-addr: 127.0.0.1:8848
        group: TRADE_GROUP
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/seata_order?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    show-sql: true

seata:
  # 属性需要seata-server端 config.txt文件中的service.vgroupMapping后的值保持一致
  # 即,nacos配置中心中要存在dataId为 service.vgroupMapping.saint-trade-tx-group 的配置
  tx-service-group: saint-trade-tx-group
  # 采用nacos作为配置中心
  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP
  # 当seata-server采用nacos做为注册中心时(通过以下信息获取seata-server实例列表)
  registry:
    type: nacos
    nacos:
      # seata-server的应用名称
      application: seata-server
      # seata-server注册到的nacos服务地址
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP

3)stock-service

stck-service整体代码结构目录如下:
在这里插入图片描述

整体包括:pom.xml、JPA数据源配置、一个Controller、一个entity、一个Dao、一个Service、一个启动类;application.yml配置文件;

1> 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>transaction-seata</artifactId>
        <groupId>com.saint</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <version>0.0.1-SNAPSHOT</version>
    <groupId>com.saint</groupId>
    <artifactId>stock-service</artifactId>
    <name>stock-service</name>

    <dependencies>

        <!--protostuff序列化依赖-->
        <dependency>
            <groupId>io.protostuff</groupId>
            <artifactId>protostuff-runtime</artifactId>
            <version>1.8.0</version>
        </dependency>
        <dependency>
            <groupId>io.protostuff</groupId>
            <artifactId>protostuff-core</artifactId>
            <version>1.8.0</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</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-seata</artifactId>
        </dependency>
    </dependencies>

</project>

2> DataSourceConfig

JPA的数据源配置和account-service中的一样;

3> StockController

package com.saint.stock.controller;

import com.saint.stock.service.StockService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author Saint
 */
@RestController
@RequiredArgsConstructor(onConstructor = @_(@Autowired))
public class StockController {

    private final StockService stockService;

    @GetMapping(path = "/deduct")
    public Boolean deduct(String commodityCode, Integer count) {
        stockService.deduct(commodityCode, count);
        return true;
    }
}

4> Stock

package com.saint.stock.entity;

import lombok.Data;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

/**
 * @author Saint
 */
@Entity
@Table(name = "stock_tbl")
@DynamicUpdate
@DynamicInsert
@Data
public class Stock {

    @Id
    private Long id;
    private String commodityCode;
    private Integer count;
}

5> StockDAO

package com.saint.stock.repository;

import com.saint.stock.entity.Stock;
import org.springframework.data.jpa.repository.JpaRepository;

/**
 * @author Saint
 */
public interface StockDAO extends JpaRepository<Stock, String> {

    Stock findByCommodityCode(String commodityCode);

}

6> StockService

package com.saint.stock.service;

import com.saint.stock.entity.Stock;
import com.saint.stock.repository.StockDAO;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * @author Saint
 */
@Service
@RequiredArgsConstructor(onConstructor = @_(@Autowired))
public class StockService {

    private final StockDAO stockDAO;

    @Transactional
    public void deduct(String commodityCode, int count) {
        Stock stock = stockDAO.findByCommodityCode(commodityCode);
        stock.setCount(stock.getCount() - count);

        stockDAO.save(stock);
    }
}

7> StockApplication

package com.saint.stock;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

/**
 * @author Saint
 */
@SpringBootApplication
@EnableJpaRepositories
public class StockApplication {
    public static void main(String[] args) {
        SpringApplication.run(StockApplication.class, args);
    }
}

8> application.yml

server:
  port: 9011
spring:
  application:
    name: stock-service
  cloud:
    nacos:
      # 这里配置的是当前服务所要注册到的Nacos地址
      discovery:
        server-addr: 127.0.0.1:8848
        group: TRADE_GROUP
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/seata_stock?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    show-sql: true

seata:
  # 属性需要seata-server端 config.txt文件中的service.vgroupMapping后的值保持一致
  # 即,nacos配置中心中要存在dataId为 service.vgroupMapping.saint-trade-tx-group 的配置
  tx-service-group: saint-trade-tx-group
  # 采用nacos作为配置中心
  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP
  # 当seata-server采用nacos做为注册中心时(通过以下信息获取seata-server实例列表)
  registry:
    type: nacos
    nacos:
      # seata-server的应用名称
      application: seata-server
      # seata-server注册到的nacos服务地址
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP

4)trade-center

trade-center整体代码结构目录如下:
在这里插入图片描述

整体包括:pom.xml、一个Controller、一个entity、两个FeignClient、一个Service、一个启动类;application.yml配置文件;

1> 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>transaction-seata</artifactId>
        <groupId>com.saint</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>trade-center</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <groupId>com.saint</groupId>
    <name>trade-center</name>

    <dependencies>
        <!--protostuff序列化依赖-->
        <dependency>
            <groupId>io.protostuff</groupId>
            <artifactId>protostuff-runtime</artifactId>
            <version>1.8.0</version>
        </dependency>
        <dependency>
            <groupId>io.protostuff</groupId>
            <artifactId>protostuff-core</artifactId>
            <version>1.8.0</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <!--负载均衡器-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-loadbalancer</artifactId>
        </dependency>

        <!--openFeign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <!--seata-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        </dependency>

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

    </dependencies>

</project>

2> TradeController

package com.saint.trade.controller;

import com.saint.trade.service.TradeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author Saint
 */
@RestController
public class TradeController {

    @Autowired
    private TradeService businessService;

    /**
     * 购买下单,模拟全局事务提交
     *
     * @return
     */
    @RequestMapping("/purchase/commit")
    public Boolean purchaseCommit() {
        businessService.purchase("1001", "2001", 1);
        return true;
    }

    /**
     * 购买下单,模拟全局事务回滚
     *
     * @return
     */
    @RequestMapping("/purchase/rollback")
    public Boolean purchaseRollback() {
        try {
            businessService.purchase("1002", "2001", 1);
//            businessService.failToPurchase("1001", "2001", 1);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }

        return true;
    }
}

3> OrderFeignClient

trade-center通过OpenFeign调用order-service;

package com.saint.trade.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

/**
 * @author Saint
 */
@FeignClient(name = "order-service")
public interface OrderFeignClient {

    @GetMapping("/create")
    void create(@RequestParam("userId") String userId, @RequestParam("commodityCode") String commodityCode,
                @RequestParam("count") Integer count);

}

4> StockFeignClient

trade-center通过OpenFeign调用stock-service;

package com.saint.trade.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

/**
 * @author Saint
 */
@FeignClient(name = "stock-service")
public interface StockFeignClient {

    @GetMapping("/deduct")
    void deduct(@RequestParam("commodityCode") String commodityCode, @RequestParam("count") Integer count);

}

5> TradeService

TradeService中通过@GlobalTransactional开启分布式事务;

package com.saint.trade.service;

import com.saint.trade.feign.OrderFeignClient;
import com.saint.trade.feign.StockFeignClient;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

/**
 * @author Saint
 */
@Service
@RequiredArgsConstructor
public class TradeService {

    private final StockFeignClient stockFeignClient;
    private final OrderFeignClient orderFeignClient;

    /**
     * 减库存,下订单
     *
     * @param userId
     * @param commodityCode
     * @param orderCount
     */
    @GlobalTransactional
    public void purchase(String userId, String commodityCode, int orderCount) {
        stockFeignClient.deduct(commodityCode, orderCount);

        orderFeignClient.create(userId, commodityCode, orderCount);
    }

    /**
     * 减库存,下订单(有异常)
     *
     * @param userId
     * @param commodityCode
     * @param orderCount
     */
    @GlobalTransactional
    public void failToPurchase(String userId, String commodityCode, int orderCount) {
        stockFeignClient.deduct(commodityCode, orderCount);
        orderFeignClient.create(userId, commodityCode, orderCount);
        throw new RuntimeException("Error!");
    }
}

6> TradeApplication

package com.saint.trade;

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

/**
 * @author Saint
 */
@SpringBootApplication
@EnableFeignClients
public class TradeApplication {

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

7> application.yml

server:
  port: 9001
spring:
  application:
    name: trade-center
  cloud:
    nacos:
      # 这里配置的是当前服务所要注册到的Nacos地址
      discovery:
        server-addr: 127.0.0.1:8848
        group: TRADE_GROUP

#  cloud:
#    alibaba:
#      seata:
#        tx-service-group: saint-trade-tx-group

seata:
  # 属性需要seata-server端 config.txt文件中的service.vgroupMapping后的值保持一致
  # 即,nacos配置中心中要存在dataId为 service.vgroupMapping.saint-trade-tx-group 的配置
  tx-service-group: saint-trade-tx-group
  # 采用nacos作为配置中心
  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP
  # 当seata-server采用nacos做为注册中心时(通过以下信息获取seata-server实例列表)
  registry:
    type: nacos
    nacos:
      # seata-server的应用名称
      application: seata-server
      # seata-server注册到的nacos服务地址
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP

4、AT模式分布式事务效果演示

分别启动trade-center、stock-service、order-service、account-service;都启动成功之后,Nacos服务信息如下:

在这里插入图片描述

四个服务的分组名称均为我们通过spring.cloud.nacos.discovery.group属性配置的;

1)请求正常

分布式事务成功,模拟正常下单、扣库存

请求访问:http://127.0.0.1:9001/purchase/commit

2)请求异常

分布式事务失败,模拟下单成功、扣库存失败,最终同时回滚

请求访问:http://127.0.0.1:9001/purchase/rollback

四、总结和后续

当前文章讲述了Spring Cloud + Nacos + Seata + OpenFeign + JPA实现分布式事务的案例。

如果你运行不起来,私信我。

官方案例文档https://github.com/Saint9768/seata-samples/tree/master/springcloud-nacos-seata;明人不说暗话,里面全是坑,基本是搭不起来的;

下一篇文章正式开启Seata源码分析,期间会穿插聊一下Seata的TCC模式实现案例;

相关实践学习
基于CentOS快速搭建LAMP环境
本教程介绍如何搭建LAMP环境,其中LAMP分别代表Linux、Apache、MySQL和PHP。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
2天前
|
监控 安全 Java
Spring cloud原理详解
Spring cloud原理详解
13 0
|
6天前
|
消息中间件 负载均衡 Java
【Spring Cloud 初探幽】
【Spring Cloud 初探幽】
14 1
|
8天前
|
Java 开发者 微服务
Spring Cloud原理详解
【5月更文挑战第4天】Spring Cloud是Spring生态系统中的微服务框架,包含配置管理、服务发现、断路器、API网关等工具,简化分布式系统开发。核心组件如Eureka(服务发现)、Config Server(配置中心)、Ribbon(负载均衡)、Hystrix(断路器)、Zuul(API网关)等。本文讨论了Spring Cloud的基本概念、核心组件、常见问题及解决策略,并提供代码示例,帮助开发者更好地理解和实践微服务架构。此外,还涵盖了服务通信方式、安全性、性能优化、自动化部署、服务网格和无服务器架构的融合等话题,揭示了微服务架构的未来趋势。
32 6
|
12天前
|
消息中间件 Java 关系型数据库
Spring事务与分布式事务
这篇文档介绍了事务的概念和数据库事务的ACID特性:原子性、一致性、隔离性和持久性。在并发环境下,事务可能出现更新丢失、脏读和不可重复读等问题,这些问题通过设置事务隔离级别(如读未提交、读已提交、可重复读和序列化)来解决。Spring事务传播行为有七种模式,影响嵌套事务的执行方式。`@Transactional`注解用于管理事务,其属性包括传播行为、隔离级别、超时和只读等。最后提到了分布式事务,分为跨库和跨服务两种情况,跨服务的分布式事务通常通过最终一致性策略,如消息队列实现。
|
12天前
|
JSON Java Apache
Spring Cloud Feign 使用Apache的HTTP Client替换Feign原生httpclient
Spring Cloud Feign 使用Apache的HTTP Client替换Feign原生httpclient
|
13天前
|
负载均衡 Java 开发者
Spring Cloud:一文读懂其原理与架构
Spring Cloud 是一套微服务解决方案,它整合了Netflix公司的多个开源框架,简化了分布式系统开发。Spring Cloud 提供了服务注册与发现、配置中心、消息总线、负载均衡、熔断机制等工具,让开发者可以快速地构建一些常见的微服务架构。
|
14天前
|
消息中间件 Java RocketMQ
Spring Cloud RocketMQ:构建可靠消息驱动的微服务架构
【4月更文挑战第28天】消息队列在微服务架构中扮演着至关重要的角色,能够实现服务之间的解耦、异步通信以及数据分发。Spring Cloud RocketMQ作为Apache RocketMQ的Spring Cloud集成,为微服务架构提供了可靠的消息传输机制。
28 1
|
14天前
|
Dubbo Java 应用服务中间件
Spring Cloud Dubbo: 微服务通信的高效解决方案
【4月更文挑战第28天】在微服务架构的发展中,服务间的高效通信至关重要。Spring Cloud Dubbo 提供了一种基于 RPC 的通信方式,使得服务间的调用就像本地方法调用一样简单。本篇博客将探讨 Spring Cloud Dubbo 的核心概念,并通过具体实例展示其在项目中的实战应用。
15 2
|
14天前
|
监控 Java Sentinel
Spring Cloud Sentinel:概念与实战应用
【4月更文挑战第28天】在分布式微服务架构中,确保系统的稳定性和可靠性至关重要。Spring Cloud Sentinel 为微服务提供流量控制、熔断降级和系统负载保护,有效预防服务雪崩。本篇博客深入探讨 Spring Cloud Sentinel 的核心概念,并通过实际案例展示其在项目中的应用。
24 0