牛又面 發表於 2025-11-27 10:59:00

Spring Cloud分布式事务(基于Seata AT模式,集成Nacos)--学习版

<center><h1>Spring Cloud分布式事务快速上手(基于Seata AT模式,集成Nacos)--学习版</h1></center>

<h2 id="前言">前言</h2>
<p>  对于从未接触过Seata的同学来说,想要快速上手Seata还是需要花费比较长的时间,因为本身微服务开发中环境的搭建、以及各种配置都已经很繁琐了,然后再集成Seata,Seata又有许多配置,对于每个微服务来说,针对Seata又有一些配置,要搞清楚各种配置之间的关系,对于像我这样的小白来说,着实不是一件容易的事。但Seata作为分布式事务的关键解决方案,在微服务架构中起着至关重要的作用。接下来,我将结合自身小白学习踩坑的过程,为大家介绍Seata的实操步骤,帮助大家少走弯路。</p>
<h2 id="依赖的相关环境及组件">依赖的相关环境及组件</h2>
<p>  为了方便像我这样的小白快速上手,我只能踩着巨人的肩膀前进,本文中相关demo基本参考ruoyi-cloud官方文档中的示例(能参考别人的代码千万别自己动手写),只做了部分改造,方便验证;同时,微服务环境框架也是直接用的ruoyi-cloud,感谢每一位开源前辈无私无畏的奉献。</p>
<center>
<table>
<thead>
<tr>
<th>组件</th>
<th>版本</th>
</tr>
</thead>
<tbody>
<tr>
<td>    ruoyi-cloud    </td>
<td>    v3.6.6    </td>
</tr>
<tr>
<td>    Seata    </td>
<td>    1.4.0    </td>
</tr>
<tr>
<td>    Nacos    </td>
<td>    2.5.0    </td>
</tr>
</tbody>
</table>
</center>
<h2 id="部署seata-server集成nacos">部署Seata-server(集成Nacos)</h2>
<h3 id="下载seata-server">下载Seata-server</h3>
<p>  可以从GitHub仓库https://github.com/apache/incubator-seata/releases下载各版本的Seata-server(直达链接:Seata-server各Releases版本)。Windows下载解压后(.zip),直接点击bin/seata-server.bat就可以启动。</p>
<h3 id="配置seata-server集成nacos">配置Seata-server,集成Nacos</h3>
<p>  使用Nacos作为注册中心及配置中心,需要修改Seata目录下conf/registry.con中的相关配置。由于使用Nacos作为注册中心,所以conf目录下的file.conf无需理会。主要修改两个地方,一个是registry.type改为nacos(默认是file),另一个是config.type改为nacos(默认是file);当然,registry.nacos,和config.nacos中需要修改成自己的环境,例如Nacos地址:127.0.0.1:8848,以及username和password,修改完成后,Seata-server就会使用Nacos作为注册中心和配置中心。</p>
<pre><code class="language-javascript">registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
loadBalance = "RandomLoadBalance"
loadBalanceVirtualNodes = 10

nacos {
    application = "seata-server"
    serverAddr = "127.0.0.1:8848"
    group = "SEATA_GROUP"
    namespace = ""
    cluster = "default"
    username = ""
    password = ""
}
eureka {
    serviceUrl = "http://localhost:8761/eureka"
    application = "default"
    weight = "1"
}
redis {
    serverAddr = "localhost:6379"
    db = 0
    password = ""
    cluster = "default"
    timeout = 0
}
zk {
    cluster = "default"
    serverAddr = "127.0.0.1:2181"
    sessionTimeout = 6000
    connectTimeout = 2000
    username = ""
    password = ""
}
consul {
    cluster = "default"
    serverAddr = "127.0.0.1:8500"
}
etcd3 {
    cluster = "default"
    serverAddr = "http://localhost:2379"
}
sofa {
    serverAddr = "127.0.0.1:9603"
    application = "default"
    region = "DEFAULT_ZONE"
    datacenter = "DefaultDataCenter"
    cluster = "default"
    group = "SEATA_GROUP"
    addressWaitTime = "3000"
}
file {
    name = "file.conf"
}
}

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

nacos {
    serverAddr = "127.0.0.1:8848"
    namespace = ""
    group = "SEATA_GROUP"
    username = ""
    password = ""
}
consul {
    serverAddr = "127.0.0.1:8500"
}
apollo {
    appId = "seata-server"
    apolloMeta = "http://192.168.1.204:8801"
    namespace = "application"
    apolloAccesskeySecret = ""
}
zk {
    serverAddr = "127.0.0.1:2181"
    sessionTimeout = 6000
    connectTimeout = 2000
    username = ""
    password = ""
}
etcd3 {
    serverAddr = "http://localhost:2379"
}
file {
    name = "file.conf"
}
}

</code></pre>
<h3 id="上传配置至nacos配置中心">上传配置至Nacos配置中心</h3>
<p>  在Nacos中新建配置,dataId为seataServer.properties,group为SEATA_GROUP(与Seata-server中config.nacos.group一致),配置内容参考https://github.com/apache/incubator-seata/tree/develop/script/config-center的config.txt并按需修改保存(直达链接:配置中心内容)。若不想手动复制,也可使用该链接目录下/nacos/nacos-config.sh或/nacos/nacos-config.py脚本导入到Nacos,这里主要修改两个地方,同时,由于后边将会搭建三个测试微服,这里需增加事务分组的配置(事务分组的具体解释请参考Seata官网https://seata.apache.org/zh-cn/docs/v1.4/user/txgroup/transaction-group):<br>
  如下修改为db,分布式事务的核心数据存储至数据库<br>
  store.mode=db<br>
  store.lock.mode=db<br>
  store.session.mode=db<br>
  以及配置数据库连接信息<br>
  store.db.driverClassName<br>
  store.db.url<br>
  store.db.user        <br>
  store.db.password<br>
  增加事务分组配置,每个服务一个,一般都采用将key 值设置为服务端的服务名,有多少个微服务就添加多少行。<br>
  #后边会搭建账户、商品、订单三个测试微服务,这配置每个微服务的事务分组<br>
  service.vgroupMapping.ruoyi-account-group=default<br>
  service.vgroupMapping.ruoyi-order-group=default<br>
  service.vgroupMapping.ruoyi-product-group=default</p>
<pre><code class="language-java">#事务分组配置
service.vgroupMapping.ruoyi-account-group=default
service.vgroupMapping.ruoyi-order-group=default
service.vgroupMapping.ruoyi-product-group=default

#For details about configuration items, see https://seata.io/zh-cn/docs/user/configurations.html
#Transport configuration, for client and server
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.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

#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.enableDegrade=false
service.disableGlobalTransaction=false

#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.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

#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.
# 修改为db,分布式事务的核心数据存储至数据库
store.mode=db
store.lock.mode=db
store.session.mode=db
#Used for password encryption
store.publicKey=

#If `store.mode,store.lock.mode,store.session.mode` are not equal to `file`, you can remove the configuration block.
store.file.dir=file_store/data
store.file.maxBranchSessionSize=16384
store.file.maxGlobalSessionSize=512
store.file.fileWriteBufferCacheSize=16384
store.file.flushDiskMode=async
store.file.sessionReloadReadSize=100

#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.jdbc.Driver
# 配置数据库连接信息
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&amp;rewriteBatchedStatements=true
store.db.user=username
store.db.password=password
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.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000

#These configurations are required if the `store mode` is `redis`. If `store.mode,store.lock.mode,store.session.mode` are not equal to `redis`, you can remove the configuration block.
store.redis.mode=single
store.redis.single.host=127.0.0.1
store.redis.single.port=6379
store.redis.sentinel.masterName=
store.redis.sentinel.sentinelHosts=
store.redis.sentinel.sentinelPassword=
store.redis.maxConn=10
store.redis.minConn=1
store.redis.maxTotal=100
store.redis.database=0
store.redis.password=
store.redis.queryLimit=100

#Transaction rule configuration, only for the server
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackFailedUnlockEnable=false
server.distributedLockExpireTime=10000
server.xaerNotaRetryTimeout=60000
server.session.branchAsyncQueueSize=5000
server.session.enableBranchAsyncRemove=false
server.enableParallelRequestHandle=false

#Metrics configuration, only for the server
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898
</code></pre>
<h3 id="导入seata-server所需sql">导入Seata-server所需SQL</h3>
<p>  前面我们已经配置了Seata使用mysql作为db高可用数据库(store.mode=db),故需要在mysql创建一个seata库(store.db.url中配置的库名),并导入数据库脚本。可以从GitHub仓库https://github.com/apache/incubator-seata/blob/develop/script/server/db下载不同数据库的SQL脚本(直达链接:mysql数据库脚本),下载后将SQL导入数据库中。</p>
<pre><code class="language-sql">-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- the table to store GlobalSession data
CREATE TABLE IF NOT EXISTS `global_table`
(
    `xid`                     VARCHAR(128) NOT NULL,
    `transaction_id`            BIGINT,
    `status`                  TINYINT      NOT NULL,
    `application_id`            VARCHAR(32),
    `transaction_service_group` VARCHAR(32),
    `transaction_name`          VARCHAR(128),
    `timeout`                   INT,
    `begin_time`                BIGINT,
    `application_data`          VARCHAR(2000),
    `gmt_create`                DATETIME,
    `gmt_modified`            DATETIME,
    PRIMARY KEY (`xid`),
    KEY `idx_status_gmt_modified` (`status` , `gmt_modified`),
    KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;

-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
    `branch_id`         BIGINT       NOT NULL,
    `xid`               VARCHAR(128) NOT NULL,
    `transaction_id`    BIGINT,
    `resource_group_id` VARCHAR(32),
    `resource_id`       VARCHAR(256),
    `branch_type`       VARCHAR(8),
    `status`            TINYINT,
    `client_id`         VARCHAR(64),
    `application_data`VARCHAR(2000),
    `gmt_create`      DATETIME(6),
    `gmt_modified`      DATETIME(6),
    PRIMARY KEY (`branch_id`),
    KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;

-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
    `row_key`      VARCHAR(128) NOT NULL,
    `xid`            VARCHAR(128),
    `transaction_id` BIGINT,
    `branch_id`      BIGINT       NOT NULL,
    `resource_id`    VARCHAR(256),
    `table_name`   VARCHAR(32),
    `pk`             VARCHAR(36),
    `status`         TINYINT      NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',
    `gmt_create`   DATETIME,
    `gmt_modified`   DATETIME,
    PRIMARY KEY (`row_key`),
    KEY `idx_status` (`status`),
    KEY `idx_branch_id` (`branch_id`),
    KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;

CREATE TABLE IF NOT EXISTS `distributed_lock`
(
    `lock_key`       CHAR(20) NOT NULL,
    `lock_value`   VARCHAR(20) NOT NULL,
    `expire`         BIGINT,
    primary key (`lock_key`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;

INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);
</code></pre>
<p>  至此,Seata-server已经配置完成并且集成了Nacos,事务数据也存储在DB中。可以点击Seata目录下bin/seata-server.bat脚本启动Seata-server。<br>
<img src="https://foruda.gitee.com/images/1688697775432625560/883302d5_1815095.png"></p>
<h2 id="搭建测试微服务">搭建测试微服务</h2>
<h3 id="创建测试库及表">创建测试库及表</h3>
<pre><code class="language-sql"># 订单数据库信息 seata_order
DROP DATABASE IF EXISTS seata_order;
CREATE DATABASE seata_order;

DROP TABLE IF EXISTS seata_order.p_order;
CREATE TABLE seata_order.p_order
(
    id               INT(11) NOT NULL AUTO_INCREMENT,
    user_id          INT(11) DEFAULT NULL,
    product_id       INT(11) DEFAULT NULL,
    amount         INT(11) DEFAULT NULL,
    total_price      DOUBLE       DEFAULT NULL,
    status         VARCHAR(100) DEFAULT NULL,
    add_time         DATETIME   DEFAULT CURRENT_TIMESTAMP,
    last_update_time DATETIME   DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (id)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8mb4;

DROP TABLE IF EXISTS seata_order.undo_log;
CREATE TABLE seata_order.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_modifiedDATETIME   NOT NULL,
    PRIMARY KEY (id),
    UNIQUE KEY ux_undo_log (xid, branch_id)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8mb4;

# 产品数据库信息 seata_product
DROP DATABASE IF EXISTS seata_product;
CREATE DATABASE seata_product;

DROP TABLE IF EXISTS seata_product.product;
CREATE TABLE seata_product.product
(
    id               INT(11) NOT NULL AUTO_INCREMENT,
    price            DOUBLE   DEFAULT NULL,
    stock            INT(11) DEFAULT NULL,
    last_update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (id)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8mb4;

DROP TABLE IF EXISTS seata_product.undo_log;
CREATE TABLE seata_product.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_modifiedDATETIME   NOT NULL,
    PRIMARY KEY (id),
    UNIQUE KEY ux_undo_log (xid, branch_id)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8mb4;

INSERT INTO seata_product.product (id, price, stock)
VALUES (1, 10, 20);


# 账户数据库信息 seata_account
DROP DATABASE IF EXISTS seata_account;
CREATE DATABASE seata_account;

DROP TABLE IF EXISTS seata_account.account;
CREATE TABLE seata_account.account
(
    id               INT(11) NOT NULL AUTO_INCREMENT,
    balance          DOUBLE   DEFAULT NULL,
    last_update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (id)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8mb4;

DROP TABLE IF EXISTS seata_account.undo_log;
CREATE TABLE seata_account.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_modifiedDATETIME   NOT NULL,
    PRIMARY KEY (id),
    UNIQUE KEY ux_undo_log (xid, branch_id)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8mb4;
INSERT INTO seata_account.account (id, balance)
VALUES (1, 50);
</code></pre>
<p>  其中,每个库中的undo_log表,是Seata AT模式必须创建的表,主要用于分支事务的回滚,undo_log表可以从GitHub仓库https://github.com/apache/incubator-seata/tree/develop/script/client/at/db下载不同数据库的SQL脚本(直达链接:mysql数据库脚本)。另外,考虑到测试方便,我们插入了一条id = 1的account记录,和一条id = 1的product记录。</p>
<h3 id="搭建测试服务">搭建测试服务</h3>
<h4 id="搭建账户服务">搭建账户服务</h4>
<p>  在ruoyi-modules新建一个Module:ruoyi-account,为了搭建方便,以及减少Maven依赖,pom依赖直接拷贝ruoyi-system中的依赖,然后新增Seata的依赖就可以了。<br>
  ruoyi-account的pom.xml:</p>
<pre><code class="language-xml">&lt;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"&gt;
    &lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;
    &lt;parent&gt;
      &lt;groupId&gt;com.ruoyi&lt;/groupId&gt;
      &lt;artifactId&gt;ruoyi-modules&lt;/artifactId&gt;
      &lt;version&gt;3.6.6&lt;/version&gt;
    &lt;/parent&gt;

    &lt;artifactId&gt;ruoyi-account&lt;/artifactId&gt;
    &lt;packaging&gt;jar&lt;/packaging&gt;

    &lt;name&gt;ruoyi-account&lt;/name&gt;
    &lt;url&gt;http://maven.apache.org&lt;/url&gt;

    &lt;properties&gt;
      &lt;project.build.sourceEncoding&gt;UTF-8&lt;/project.build.sourceEncoding&gt;
    &lt;/properties&gt;

    &lt;dependencies&gt;
      &lt;!-- SpringCloud Alibaba Nacos --&gt;
      &lt;dependency&gt;
            &lt;groupId&gt;com.alibaba.cloud&lt;/groupId&gt;
            &lt;artifactId&gt;spring-cloud-starter-alibaba-nacos-discovery&lt;/artifactId&gt;
      &lt;/dependency&gt;

      &lt;!-- SpringCloud Alibaba Nacos Config --&gt;
      &lt;dependency&gt;
            &lt;groupId&gt;com.alibaba.cloud&lt;/groupId&gt;
            &lt;artifactId&gt;spring-cloud-starter-alibaba-nacos-config&lt;/artifactId&gt;
      &lt;/dependency&gt;

      &lt;!-- SpringCloud Alibaba Sentinel --&gt;
      &lt;dependency&gt;
            &lt;groupId&gt;com.alibaba.cloud&lt;/groupId&gt;
            &lt;artifactId&gt;spring-cloud-starter-alibaba-sentinel&lt;/artifactId&gt;
      &lt;/dependency&gt;

      &lt;!-- SpringBoot Actuator --&gt;
      &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-actuator&lt;/artifactId&gt;
      &lt;/dependency&gt;

      &lt;!-- Mysql Connector --&gt;
      &lt;dependency&gt;
            &lt;groupId&gt;com.mysql&lt;/groupId&gt;
            &lt;artifactId&gt;mysql-connector-j&lt;/artifactId&gt;
      &lt;/dependency&gt;

      &lt;!-- RuoYi Common DataSource --&gt;
      &lt;dependency&gt;
            &lt;groupId&gt;com.ruoyi&lt;/groupId&gt;
            &lt;artifactId&gt;ruoyi-common-datasource&lt;/artifactId&gt;
      &lt;/dependency&gt;

      &lt;!-- RuoYi Common DataScope --&gt;
      &lt;dependency&gt;
            &lt;groupId&gt;com.ruoyi&lt;/groupId&gt;
            &lt;artifactId&gt;ruoyi-common-datascope&lt;/artifactId&gt;
      &lt;/dependency&gt;

      &lt;!-- RuoYi Common Log --&gt;
      &lt;dependency&gt;
            &lt;groupId&gt;com.ruoyi&lt;/groupId&gt;
            &lt;artifactId&gt;ruoyi-common-log&lt;/artifactId&gt;
      &lt;/dependency&gt;

      &lt;!-- RuoYi Common Swagger --&gt;
      &lt;dependency&gt;
            &lt;groupId&gt;com.ruoyi&lt;/groupId&gt;
            &lt;artifactId&gt;ruoyi-common-swagger&lt;/artifactId&gt;
      &lt;/dependency&gt;

      &lt;!-- SpringBoot Seata --&gt;
      &lt;dependency&gt;
            &lt;groupId&gt;com.alibaba.cloud&lt;/groupId&gt;
            &lt;artifactId&gt;spring-cloud-starter-alibaba-seata&lt;/artifactId&gt;
      &lt;/dependency&gt;

      &lt;dependency&gt;
            &lt;groupId&gt;org.projectlombok&lt;/groupId&gt;
            &lt;artifactId&gt;lombok&lt;/artifactId&gt;
      &lt;/dependency&gt;


    &lt;/dependencies&gt;

    &lt;build&gt;
      &lt;finalName&gt;${project.artifactId}&lt;/finalName&gt;
      &lt;plugins&gt;
            &lt;plugin&gt;
                &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
                &lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;
                &lt;executions&gt;
                  &lt;execution&gt;
                        &lt;goals&gt;
                            &lt;goal&gt;repackage&lt;/goal&gt;
                        &lt;/goals&gt;
                  &lt;/execution&gt;
                &lt;/executions&gt;
            &lt;/plugin&gt;
      &lt;/plugins&gt;
    &lt;/build&gt;
&lt;/project&gt;

</code></pre>
<p>  ruoyi-account的bootstrap.yml:</p>
<pre><code class="language-yaml"># Tomcat
server:
port: 10300

# Spring
spring:
application:
    # 应用名称
    name: ruoyi-account
profiles:
    # 环境配置
    active: dev
cloud:
    nacos:
      discovery:
      # 服务注册地址
      server-addr: 127.0.0.1:8848
      config:
      # 配置中心地址
      server-addr: 127.0.0.1:8848
      # 配置文件格式
      file-extension: yml
      # 共享配置
      shared-configs:
          - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
seata:
enabled: true
# Seata 应用编号,默认为 ${spring.application.name}
application-id: ${spring.application.name}
# Seata 事务组编号,用于 TC 集群名
tx-service-group: ${spring.application.name}-group
# 关闭自动代理
enable-auto-data-source-proxy: false
# 服务配置项
service:
    # 虚拟组和分组的映射
    vgroup-mapping:
      ruoyi-account-group: default
config:
    type: nacos
    nacos:
      serverAddr: 127.0.0.1:8848
      group: SEATA_GROUP
      namespace:
      dataId: seataServer.properties
registry:
    type: nacos
    nacos:
      application: seata-server
      server-addr: 127.0.0.1:8848
      namespace:

</code></pre>
<p>  ruoyi-account的bootstrap.yml也是直接拷贝ruoyi-system中的bootstrap.yml,然后修改服务端口,服务名;为了演示方便,关于Seata的配置也直接写在了bootstrap.yml。<br>
  ruoyi-account的ruoyi-account-dev.yml:</p>
<pre><code class="language-yaml"># spring配置
spring:
redis:
    host: localhost
    port: 6379
    password:
datasource:
    druid:
      stat-view-servlet:
      enabled: true
      loginUsername: ruoyi
      loginPassword: 123456
    dynamic:
      druid:
      initial-size: 5
      min-idle: 5
      maxActive: 20
      maxWait: 60000
      connectTimeout: 30000
      socketTimeout: 60000
      timeBetweenEvictionRunsMillis: 60000
      minEvictableIdleTimeMillis: 300000
      validationQuery: SELECT 1 FROM DUAL
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      poolPreparedStatements: true
      maxPoolPreparedStatementPerConnectionSize: 20
      filters: stat,slf4j
      connectionProperties: druid.stat.mergeSql\=true;druid.stat.slowSqlMillis\=5000
      datasource:
          # 主库数据源
          master:
            driver-class-name: com.mysql.cj.jdbc.Driver
            url: jdbc:mysql://localhost:3306/seata_account?useUnicode=true&amp;characterEncoding=utf8&amp;zeroDateTimeBehavior=convertToNull&amp;useSSL=true&amp;serverTimezone=GMT%2B8
            username: root
            password: root123
          # 从库数据源
          # slave:
            # username:
            # password:
            # url:
            # driver-class-name:
      seata: true #开启seata代理,开启后默认每个数据源都代理,如果某个不需要代理可单独关闭

# mybatis配置
mybatis:
    # 搜索指定包别名
    typeAliasesPackage: com.ruoyi.account
    # 配置mapper的扫描,找到所有的mapper.xml映射文件
    mapperLocations: classpath:mapper/**/*.xml

# springdoc配置
springdoc:
gatewayUrl: http://localhost:8080/${spring.application.name}
api-docs:
    # 是否开启接口文档
    enabled: false

</code></pre>
<p>  ruoyi-account的ruoyi-account-dev.yml也是直接拷贝ruoyi-system中的ruoyi-system-dev.yml,仅修改数据库连接信息,MyBatis扫描的包等,唯一比较重要的一点是动态数据源这里配置了seata:true,开启Seata代理。<br>
  ruoyi-account示例代码:<br>
<strong>  Account.java</strong></p>
<pre><code class="language-java">
package com.ruoyi.account.domain;

import lombok.Getter;
import lombok.Setter;

import java.util.Date;

@Getter
@Setter
public class Account {

    private Long id;

    /**
   * 余额
   */
    private Double balance;

    private Date lastUpdateTime;

}

</code></pre>
<p><strong>  AccountMapper.java</strong></p>
<pre><code class="language-java">package com.ruoyi.account.mapper;

import com.ruoyi.account.domain.Account;

public interface AccountMapper {
    public Account selectById(Long userId);

    public void updateById(Account account);
}
</code></pre>
<p><strong>  AccountMapper.xml</strong></p>
<pre><code class="language-xml">&lt;?xml version="1.0" encoding="UTF-8" ?&gt;
&lt;!DOCTYPE mapper
      PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
      "http://mybatis.org/dtd/mybatis-3-mapper.dtd"&gt;
&lt;mapper namespace="com.ruoyi.account.mapper.AccountMapper"&gt;

    &lt;resultMap type="com.ruoyi.account.domain.Account" id="AccountResult"&gt;
      &lt;id   property="id"            column="id"                /&gt;
      &lt;result property="balance"         column="balance"         /&gt;
      &lt;result property="lastUpdateTime"column="last_update_time"/&gt;
    &lt;/resultMap&gt;

    &lt;select id="selectById" parameterType="com.ruoyi.account.domain.Account" resultMap="AccountResult"&gt;
      select id, balance, last_update_time
      from account where id = #{userId}
    &lt;/select&gt;

    &lt;update id="updateById" parameterType="com.ruoyi.account.domain.Account"&gt;
      update account set balance = #{balance}, last_update_time = sysdate() where id = #{id}
    &lt;/update&gt;

&lt;/mapper&gt;
</code></pre>
<p><strong>  AccountService.java</strong></p>
<pre><code class="language-java">package com.ruoyi.account.service;

public interface AccountService {
    /**
   * 账户扣减
   * @param userId 用户 ID
   * @param price 扣减金额
   */
    void reduceBalance(Long userId, Double price);
}
</code></pre>
<p><strong>  AccountServiceImpl.java</strong></p>
<pre><code class="language-java">package com.ruoyi.account.service.impl;

import javax.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import com.baomidou.dynamic.datasource.annotation.DS;
import com.ruoyi.account.domain.Account;
import com.ruoyi.account.mapper.AccountMapper;
import com.ruoyi.account.service.AccountService;
import io.seata.core.context.RootContext;

@Service
public class AccountServiceImpl implements AccountService
{
    private static final Logger log = LoggerFactory.getLogger(AccountServiceImpl.class);
   
    @Resource
    private AccountMapper accountMapper;

    /**
   * 事务传播特性设置为 REQUIRES_NEW 开启新的事务 重要!!!!一定要使用REQUIRES_NEW
   * 在若依的示例中,事务的传播特性必须设置REQUIRES_NEW,但经本人测试,多服务调用事务
   * 设置成默认的REQUIRED也能回滚成功
   */
    @Override
    @Transactional
    public void reduceBalance(Long userId, Double price)
    {
      log.info("=============ACCOUNT START=================");
      log.info("当前 XID: {}", RootContext.getXID());

      Account account = accountMapper.selectById(userId);
      Double balance = account.getBalance();
      log.info("下单用户{}余额为 {},商品总价为{}", userId, balance, price);

      if (balance &lt; price)
      {
            log.warn("用户 {} 余额不足,当前余额:{}", userId, balance);
            throw new RuntimeException("余额不足");
      }
      log.info("开始扣减用户 {} 余额", userId);
      double currentBalance = account.getBalance() - price;
      account.setBalance(currentBalance);
      accountMapper.updateById(account);
      log.info("扣减用户 {} 余额成功,扣减后用户账户余额为{}", userId, currentBalance);
      log.info("=============ACCOUNT END=================");
    }

}
</code></pre>
<p><strong>  AccountController.java</strong></p>
<pre><code class="language-java">package com.ruoyi.account.controller;

import com.ruoyi.account.dto.ReduceBalanceRequest;
import com.ruoyi.account.service.AccountService;
import com.ruoyi.common.core.domain.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/account")
public class AccountController {

    @Autowired
    private AccountService accountService;

    @PostMapping("/reduceBalance")
    public R reduceBalance(@Validated @RequestBody ReduceBalanceRequest request) {
      accountService.reduceBalance(request.getUserId(), request.getPrice());
      return R.ok("下单成功");
    }
}
</code></pre>
<p><strong>  ReduceBalanceRequest.java</strong></p>
<pre><code class="language-java">package com.ruoyi.account.dto;


import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ReduceBalanceRequest {
    private Long userId;

    private Double price;
}
</code></pre>
<p>  至此,账户服务就搭建好了,这里主要提供一个扣减账户余额的接口。</p>
<h4 id="搭建商品服务">搭建商品服务</h4>
<p>  同样,在ruoyi-modules新建一个Module:ruoyi-product。<br>
  ruoyi-product的pom.xml:</p>
<pre><code class="language-xml">&lt;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"&gt;
    &lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;
    &lt;parent&gt;
      &lt;groupId&gt;com.ruoyi&lt;/groupId&gt;
      &lt;artifactId&gt;ruoyi-modules&lt;/artifactId&gt;
      &lt;version&gt;3.6.6&lt;/version&gt;
    &lt;/parent&gt;

    &lt;artifactId&gt;ruoyi-product&lt;/artifactId&gt;
    &lt;packaging&gt;jar&lt;/packaging&gt;

    &lt;name&gt;ruoyi-product&lt;/name&gt;
    &lt;url&gt;http://maven.apache.org&lt;/url&gt;

    &lt;properties&gt;
      &lt;project.build.sourceEncoding&gt;UTF-8&lt;/project.build.sourceEncoding&gt;
    &lt;/properties&gt;

    &lt;dependencies&gt;
      &lt;!-- SpringCloud Alibaba Nacos --&gt;
      &lt;dependency&gt;
            &lt;groupId&gt;com.alibaba.cloud&lt;/groupId&gt;
            &lt;artifactId&gt;spring-cloud-starter-alibaba-nacos-discovery&lt;/artifactId&gt;
      &lt;/dependency&gt;

      &lt;!-- SpringCloud Alibaba Nacos Config --&gt;
      &lt;dependency&gt;
            &lt;groupId&gt;com.alibaba.cloud&lt;/groupId&gt;
            &lt;artifactId&gt;spring-cloud-starter-alibaba-nacos-config&lt;/artifactId&gt;
      &lt;/dependency&gt;

      &lt;!-- SpringCloud Alibaba Sentinel --&gt;
      &lt;dependency&gt;
            &lt;groupId&gt;com.alibaba.cloud&lt;/groupId&gt;
            &lt;artifactId&gt;spring-cloud-starter-alibaba-sentinel&lt;/artifactId&gt;
      &lt;/dependency&gt;

      &lt;!-- SpringBoot Actuator --&gt;
      &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-actuator&lt;/artifactId&gt;
      &lt;/dependency&gt;

      &lt;!-- Mysql Connector --&gt;
      &lt;dependency&gt;
            &lt;groupId&gt;com.mysql&lt;/groupId&gt;
            &lt;artifactId&gt;mysql-connector-j&lt;/artifactId&gt;
      &lt;/dependency&gt;

      &lt;!-- RuoYi Common DataSource --&gt;
      &lt;dependency&gt;
            &lt;groupId&gt;com.ruoyi&lt;/groupId&gt;
            &lt;artifactId&gt;ruoyi-common-datasource&lt;/artifactId&gt;
      &lt;/dependency&gt;

      &lt;!-- RuoYi Common DataScope --&gt;
      &lt;dependency&gt;
            &lt;groupId&gt;com.ruoyi&lt;/groupId&gt;
            &lt;artifactId&gt;ruoyi-common-datascope&lt;/artifactId&gt;
      &lt;/dependency&gt;

      &lt;!-- RuoYi Common Log --&gt;
      &lt;dependency&gt;
            &lt;groupId&gt;com.ruoyi&lt;/groupId&gt;
            &lt;artifactId&gt;ruoyi-common-log&lt;/artifactId&gt;
      &lt;/dependency&gt;

      &lt;!-- RuoYi Common Swagger --&gt;
      &lt;dependency&gt;
            &lt;groupId&gt;com.ruoyi&lt;/groupId&gt;
            &lt;artifactId&gt;ruoyi-common-swagger&lt;/artifactId&gt;
      &lt;/dependency&gt;

      &lt;!-- SpringBoot Seata --&gt;
      &lt;dependency&gt;
            &lt;groupId&gt;com.alibaba.cloud&lt;/groupId&gt;
            &lt;artifactId&gt;spring-cloud-starter-alibaba-seata&lt;/artifactId&gt;
      &lt;/dependency&gt;

      &lt;dependency&gt;
            &lt;groupId&gt;org.projectlombok&lt;/groupId&gt;
            &lt;artifactId&gt;lombok&lt;/artifactId&gt;
      &lt;/dependency&gt;
    &lt;/dependencies&gt;

    &lt;build&gt;
      &lt;finalName&gt;${project.artifactId}&lt;/finalName&gt;
      &lt;plugins&gt;
            &lt;plugin&gt;
                &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
                &lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;
                &lt;executions&gt;
                  &lt;execution&gt;
                        &lt;goals&gt;
                            &lt;goal&gt;repackage&lt;/goal&gt;
                        &lt;/goals&gt;
                  &lt;/execution&gt;
                &lt;/executions&gt;
            &lt;/plugin&gt;
      &lt;/plugins&gt;
    &lt;/build&gt;
&lt;/project&gt;


</code></pre>
<p>  ruoyi-product的bootstrap.yml:</p>
<pre><code class="language-yaml"># Tomcat
server:
port: 10302

# Spring
spring:
application:
    # 应用名称
    name: ruoyi-product
profiles:
    # 环境配置
    active: dev
cloud:
    nacos:
      discovery:
      # 服务注册地址
      server-addr: 127.0.0.1:8848
      config:
      # 配置中心地址
      server-addr: 127.0.0.1:8848
      # 配置文件格式
      file-extension: yml
      # 共享配置
      shared-configs:
          - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
seata:
enabled: true
# Seata 应用编号,默认为 ${spring.application.name}
application-id: ${spring.application.name}
# Seata 事务组编号,用于 TC 集群名
tx-service-group: ${spring.application.name}-group
# 关闭自动代理
enable-auto-data-source-proxy: false
# 服务配置项
service:
    # 虚拟组和分组的映射
    vgroup-mapping:
      ruoyi-product-group: default
config:
    type: nacos
    nacos:
      serverAddr: 127.0.0.1:8848
      group: SEATA_GROUP
      namespace:
      dataId: seataServer.properties
registry:
    type: nacos
    nacos:
      application: seata-server
      server-addr: 127.0.0.1:8848
      namespace:

</code></pre>
<p>  ruoyi-product的ruoyi-product-dev.yml:</p>
<pre><code class="language-yaml"># spring配置
spring:
redis:
    host: localhost
    port: 6379
    password:
datasource:
    druid:
      stat-view-servlet:
      enabled: true
      loginUsername: ruoyi
      loginPassword: 123456
    dynamic:
      druid:
      initial-size: 5
      min-idle: 5
      maxActive: 20
      maxWait: 60000
      connectTimeout: 30000
      socketTimeout: 60000
      timeBetweenEvictionRunsMillis: 60000
      minEvictableIdleTimeMillis: 300000
      validationQuery: SELECT 1 FROM DUAL
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      poolPreparedStatements: true
      maxPoolPreparedStatementPerConnectionSize: 20
      filters: stat,slf4j
      connectionProperties: druid.stat.mergeSql\=true;druid.stat.slowSqlMillis\=5000
      datasource:
          # 主库数据源
          master:
            driver-class-name: com.mysql.cj.jdbc.Driver
            url: jdbc:mysql://localhost:3306/seata_product?useUnicode=true&amp;characterEncoding=utf8&amp;zeroDateTimeBehavior=convertToNull&amp;useSSL=true&amp;serverTimezone=GMT%2B8
            username: root
            password: root123
          # 从库数据源
          # slave:
            # username:
            # password:
            # url:
            # driver-class-name:
      seata: true

# mybatis配置
mybatis:
    # 搜索指定包别名
    typeAliasesPackage: com.ruoyi.product
    # 配置mapper的扫描,找到所有的mapper.xml映射文件
    mapperLocations: classpath:mapper/**/*.xml

# springdoc配置
springdoc:
gatewayUrl: http://localhost:8080/${spring.application.name}
api-docs:
    # 是否开启接口文档
    enabled: false

</code></pre>
<p>  ruoyi-product示例代码:<br>
<strong>  Product.java</strong></p>
<pre><code class="language-java">
package com.ruoyi.product.domain;

import lombok.Getter;
import lombok.Setter;

import java.util.Date;

@Getter
@Setter
public class Product {

    private Integer id;
    /**
   * 价格
   */
    private Double price;
    /**
   * 库存
   */
    private Integer stock;

    private Date lastUpdateTime;

}
</code></pre>
<p><strong>  ProductMapper.java</strong></p>
<pre><code class="language-java">package com.ruoyi.product.mapper;

import com.ruoyi.product.domain.Product;

public interface ProductMapper {
    public Product selectById(Long productId);

    public void updateById(Product product);
}
</code></pre>
<p><strong>  ProductMapper.xml</strong></p>
<pre><code class="language-xml">&lt;?xml version="1.0" encoding="UTF-8" ?&gt;
&lt;!DOCTYPE mapper
      PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
      "http://mybatis.org/dtd/mybatis-3-mapper.dtd"&gt;
&lt;mapper namespace="com.ruoyi.product.mapper.ProductMapper"&gt;

    &lt;resultMap type="com.ruoyi.product.domain.Product" id="ProductResult"&gt;
      &lt;id   property="id"            column="id"                /&gt;
      &lt;result property="price"         column="price"             /&gt;
      &lt;result property="stock"         column="stock"             /&gt;
      &lt;result property="lastUpdateTime"column="last_update_time"/&gt;
    &lt;/resultMap&gt;

    &lt;select id="selectById" parameterType="com.ruoyi.product.domain.Product" resultMap="ProductResult"&gt;
      select id, price, stock, last_update_time
      from product where id = #{productId}
    &lt;/select&gt;

    &lt;update id="updateById" parameterType="com.ruoyi.product.domain.Product"&gt;
      update product set price = #{price}, stock = #{stock}, last_update_time = sysdate() where id = #{id}
    &lt;/update&gt;

&lt;/mapper&gt;
</code></pre>
<p><strong>  ProductService.java</strong></p>
<pre><code class="language-java">package com.ruoyi.product.service;

public interface ProductService {
    /**
   * 扣减库存
   *
   * @param productId 商品 ID
   * @param amount 扣减数量
   * @return 商品总价
   */
    Double reduceStock(Long productId, Integer amount);
}
</code></pre>
<p><strong>  ProductServiceImpl.java</strong></p>
<pre><code class="language-java">package com.ruoyi.product.service.impl;

import javax.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import com.baomidou.dynamic.datasource.annotation.DS;
import com.ruoyi.product.domain.Product;
import com.ruoyi.product.mapper.ProductMapper;
import com.ruoyi.product.service.ProductService;
import io.seata.core.context.RootContext;

@Service
public class ProductServiceImpl implements ProductService {
    private static final Logger log = LoggerFactory.getLogger(ProductServiceImpl.class);

    @Resource
    private ProductMapper productMapper;

    /**
   * 事务传播特性设置为 REQUIRES_NEW 开启新的事务 重要!!!!一定要使用REQUIRES_NEW
   * 在若依的示例中,事务的传播特性必须设置REQUIRES_NEW,但经本人测试,多服务调用事务
   * 设置成默认的REQUIRED也能回滚成功
   */
    @Transactional
    @Override
    public Double reduceStock(Long productId, Integer amount)
    {
      log.info("=============PRODUCT START=================");
      log.info("当前 XID: {}", RootContext.getXID());

      // 检查库存
      Product product = productMapper.selectById(productId);
      Integer stock = product.getStock();
      log.info("商品编号为 {} 的库存为{},订单商品数量为{}", productId, stock, amount);

      if (stock &lt; amount)
      {
            log.warn("商品编号为{} 库存不足,当前库存:{}", productId, stock);
            throw new RuntimeException("库存不足");
      }
      log.info("开始扣减商品编号为 {} 库存,单价商品价格为{}", productId, product.getPrice());
      // 扣减库存
      int currentStock = stock - amount;
      product.setStock(currentStock);
      productMapper.updateById(product);
      double totalPrice = product.getPrice() * amount;
      log.info("扣减商品编号为 {} 库存成功,扣减后库存为{}, {} 件商品总价为 {} ", productId, currentStock, amount, totalPrice);
      log.info("=============PRODUCT END=================");
      return totalPrice;
    }

}
</code></pre>
<p><strong>  ProductController.java</strong></p>
<pre><code class="language-java">package com.ruoyi.product.controller;

import com.ruoyi.common.core.domain.R;
import com.ruoyi.product.dto.ReduceStockRequest;
import com.ruoyi.product.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/product")
public class ProductController {

    @Autowired
    private ProductService productService;

    @PostMapping("/reduceStock")
    public R&lt;Double&gt; reduceStock(@Validated @RequestBody ReduceStockRequest request) {
      Double d = productService.reduceStock(request.getProductId(), request.getAmount());
      return R.ok(d);
    }
}

</code></pre>
<p><strong>  ReduceStockRequest.java</strong></p>
<pre><code class="language-java">package com.ruoyi.product.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ReduceStockRequest {
    private Long productId;

    private Integer amount;

}
</code></pre>
<p>  至此,商品服务就搭建好了,这里主要提供一个扣减商品库存的接口。</p>
<h4 id="创建服务调用模块">创建服务调用模块</h4>
<p>  在ruoyi-modules新建一个Module:ruoyi-call(当你也可以不新建Module,直接把Feign接口写在具调用的服务中,这里新建Module主是为了更好的体现模块之间的低耦合),该模块主要的作用是提供Feign接口,用于订单服务调用商品及账户服务,为了搭建方便,以及减少Maven依赖,pom依赖直接拷贝ruoyi-api-system中的依赖。<br>
  ruoyi-call的pom.xml:</p>
<pre><code class="language-xml">&lt;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"&gt;
    &lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;
    &lt;parent&gt;
      &lt;groupId&gt;com.ruoyi&lt;/groupId&gt;
      &lt;artifactId&gt;ruoyi-modules&lt;/artifactId&gt;
      &lt;version&gt;3.6.6&lt;/version&gt;
    &lt;/parent&gt;

    &lt;artifactId&gt;ruoyi-call&lt;/artifactId&gt;
    &lt;packaging&gt;jar&lt;/packaging&gt;

    &lt;name&gt;ruoyi-call&lt;/name&gt;
    &lt;url&gt;http://maven.apache.org&lt;/url&gt;

    &lt;properties&gt;
      &lt;project.build.sourceEncoding&gt;UTF-8&lt;/project.build.sourceEncoding&gt;
    &lt;/properties&gt;

    &lt;dependencies&gt;
      &lt;dependency&gt;
            &lt;groupId&gt;com.ruoyi&lt;/groupId&gt;
            &lt;artifactId&gt;ruoyi-common-core&lt;/artifactId&gt;
      &lt;/dependency&gt;

      &lt;dependency&gt;
            &lt;groupId&gt;org.projectlombok&lt;/groupId&gt;
            &lt;artifactId&gt;lombok&lt;/artifactId&gt;
      &lt;/dependency&gt;
    &lt;/dependencies&gt;
&lt;/project&gt;

</code></pre>
<p>  ruoyi-call示例代码:<br>
<strong>  AccountFeignService.java</strong></p>
<pre><code class="language-java">
package com.ruoyi.call.feign;

import com.ruoyi.call.dto.ReduceBalanceRequest;
import com.ruoyi.common.core.domain.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

@FeignClient(name = "ruoyi-account")
public interface AccountFeignService {

    @PostMapping("/account/reduceBalance")
    R reduceBalance(@RequestBody ReduceBalanceRequest request);
}

</code></pre>
<p><strong>  ProductFeignService.java</strong></p>
<pre><code class="language-java">package com.ruoyi.call.feign;

import com.ruoyi.call.dto.ReduceStockRequest;
import com.ruoyi.common.core.domain.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

@FeignClient(name = "ruoyi-product")
public interface ProductFeignService {

    @PostMapping("/product/reduceStock")
    R&lt;Double&gt; reduceStock(@RequestBody ReduceStockRequest request);
}

</code></pre>
<p><strong>  ReduceBalanceRequest.java</strong></p>
<pre><code class="language-java">package com.ruoyi.call.dto;


import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ReduceBalanceRequest {
    private Long userId;

    private Double price;
}
</code></pre>
<p><strong>  ReduceStockRequest.java</strong></p>
<pre><code class="language-java">package com.ruoyi.call.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ReduceStockRequest {
    private Long productId;

    private Integer amount;

}
</code></pre>
<p>  至此,服务调用模块就创建好了,这里主要提供Feign接口,供订单服务调用。</p>
<h4 id="搭建订单服务">搭建订单服务</h4>
<p>  在ruoyi-modules新建一个Module:ruoyi-order,为了搭建方便,以及减少Maven依赖,pom依赖直接拷贝ruoyi-system中的依赖,然后新增Seata的依赖,再就是引入创刚建的服务调用模块。<br>
  ruoyi-order的pom.xml:</p>
<pre><code class="language-xml">&lt;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"&gt;
    &lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;
    &lt;parent&gt;
      &lt;groupId&gt;com.ruoyi&lt;/groupId&gt;
      &lt;artifactId&gt;ruoyi-modules&lt;/artifactId&gt;
      &lt;version&gt;3.6.6&lt;/version&gt;
    &lt;/parent&gt;

    &lt;artifactId&gt;ruoyi-order&lt;/artifactId&gt;
    &lt;packaging&gt;jar&lt;/packaging&gt;

    &lt;name&gt;ruoyi-order&lt;/name&gt;
    &lt;url&gt;http://maven.apache.org&lt;/url&gt;

    &lt;properties&gt;
      &lt;project.build.sourceEncoding&gt;UTF-8&lt;/project.build.sourceEncoding&gt;
    &lt;/properties&gt;

    &lt;dependencies&gt;
      &lt;!-- SpringCloud Alibaba Nacos --&gt;
      &lt;dependency&gt;
            &lt;groupId&gt;com.alibaba.cloud&lt;/groupId&gt;
            &lt;artifactId&gt;spring-cloud-starter-alibaba-nacos-discovery&lt;/artifactId&gt;
      &lt;/dependency&gt;

      &lt;!-- SpringCloud Alibaba Nacos Config --&gt;
      &lt;dependency&gt;
            &lt;groupId&gt;com.alibaba.cloud&lt;/groupId&gt;
            &lt;artifactId&gt;spring-cloud-starter-alibaba-nacos-config&lt;/artifactId&gt;
      &lt;/dependency&gt;

      &lt;!-- SpringCloud Alibaba Sentinel --&gt;
      &lt;dependency&gt;
            &lt;groupId&gt;com.alibaba.cloud&lt;/groupId&gt;
            &lt;artifactId&gt;spring-cloud-starter-alibaba-sentinel&lt;/artifactId&gt;
      &lt;/dependency&gt;

      &lt;!-- SpringBoot Actuator --&gt;
      &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-actuator&lt;/artifactId&gt;
      &lt;/dependency&gt;

      &lt;!-- Mysql Connector --&gt;
      &lt;dependency&gt;
            &lt;groupId&gt;com.mysql&lt;/groupId&gt;
            &lt;artifactId&gt;mysql-connector-j&lt;/artifactId&gt;
      &lt;/dependency&gt;

      &lt;!-- RuoYi Common DataSource --&gt;
      &lt;dependency&gt;
            &lt;groupId&gt;com.ruoyi&lt;/groupId&gt;
            &lt;artifactId&gt;ruoyi-common-datasource&lt;/artifactId&gt;
      &lt;/dependency&gt;

      &lt;!-- RuoYi Common DataScope --&gt;
      &lt;dependency&gt;
            &lt;groupId&gt;com.ruoyi&lt;/groupId&gt;
            &lt;artifactId&gt;ruoyi-common-datascope&lt;/artifactId&gt;
      &lt;/dependency&gt;

      &lt;!-- RuoYi Common Log --&gt;
      &lt;dependency&gt;
            &lt;groupId&gt;com.ruoyi&lt;/groupId&gt;
            &lt;artifactId&gt;ruoyi-common-log&lt;/artifactId&gt;
      &lt;/dependency&gt;

      &lt;!-- RuoYi Common Swagger --&gt;
      &lt;dependency&gt;
            &lt;groupId&gt;com.ruoyi&lt;/groupId&gt;
            &lt;artifactId&gt;ruoyi-common-swagger&lt;/artifactId&gt;
      &lt;/dependency&gt;

      &lt;!-- SpringBoot Seata --&gt;
      &lt;dependency&gt;
            &lt;groupId&gt;com.alibaba.cloud&lt;/groupId&gt;
            &lt;artifactId&gt;spring-cloud-starter-alibaba-seata&lt;/artifactId&gt;
      &lt;/dependency&gt;

      &lt;dependency&gt;
            &lt;groupId&gt;com.ruoyi&lt;/groupId&gt;
            &lt;artifactId&gt;ruoyi-call&lt;/artifactId&gt;
            &lt;version&gt;3.6.6&lt;/version&gt;
      &lt;/dependency&gt;
    &lt;/dependencies&gt;

    &lt;build&gt;
      &lt;finalName&gt;${project.artifactId}&lt;/finalName&gt;
      &lt;plugins&gt;
            &lt;plugin&gt;
                &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
                &lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;
                &lt;executions&gt;
                  &lt;execution&gt;
                        &lt;goals&gt;
                            &lt;goal&gt;repackage&lt;/goal&gt;
                        &lt;/goals&gt;
                  &lt;/execution&gt;
                &lt;/executions&gt;
            &lt;/plugin&gt;
      &lt;/plugins&gt;
    &lt;/build&gt;
&lt;/project&gt;

</code></pre>
<p>  ruoyi-order的bootstrap.yml:</p>
<pre><code class="language-yaml"># Tomcat
server:
port: 10301

# Spring
spring:
application:
    # 应用名称
    name: ruoyi-order
profiles:
    # 环境配置
    active: dev
cloud:
    nacos:
      discovery:
      # 服务注册地址
      server-addr: 127.0.0.1:8848
      config:
      # 配置中心地址
      server-addr: 127.0.0.1:8848
      # 配置文件格式
      file-extension: yml
      # 共享配置
      shared-configs:
          - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
seata:
enabled: true
# Seata 应用编号,默认为 ${spring.application.name}
application-id: ${spring.application.name}
# Seata 事务组编号,用于 TC 集群名
tx-service-group: ${spring.application.name}-group
# 关闭自动代理
enable-auto-data-source-proxy: false
# 服务配置项
service:
    # 虚拟组和分组的映射
    vgroup-mapping:
      ruoyi-order-group: default
config:
    type: nacos
    nacos:
      serverAddr: 127.0.0.1:8848
      group: SEATA_GROUP
      namespace:
      dataId: seataServer.properties
registry:
    type: nacos
    nacos:
      application: seata-server
      server-addr: 127.0.0.1:8848
      namespace:

</code></pre>
<p>  ruoyi-order的ruoyi-order-dev.yml:</p>
<pre><code class="language-yaml"># spring配置
spring:
redis:
    host: localhost
    port: 6379
    password:
datasource:
    druid:
      stat-view-servlet:
      enabled: true
      loginUsername: ruoyi
      loginPassword: 123456
    dynamic:
      druid:
      initial-size: 5
      min-idle: 5
      maxActive: 20
      maxWait: 60000
      connectTimeout: 30000
      socketTimeout: 60000
      timeBetweenEvictionRunsMillis: 60000
      minEvictableIdleTimeMillis: 300000
      validationQuery: SELECT 1 FROM DUAL
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      poolPreparedStatements: true
      maxPoolPreparedStatementPerConnectionSize: 20
      filters: stat,slf4j
      connectionProperties: druid.stat.mergeSql\=true;druid.stat.slowSqlMillis\=5000
      datasource:
          # 主库数据源
          master:
            driver-class-name: com.mysql.cj.jdbc.Driver
            url: jdbc:mysql://localhost:3306/seata_order?useUnicode=true&amp;characterEncoding=utf8&amp;zeroDateTimeBehavior=convertToNull&amp;useSSL=true&amp;serverTimezone=GMT%2B8
            username: root
            password: root123
          # 从库数据源
          # slave:
            # username:
            # password:
            # url:
            # driver-class-name:
      seata: true

# mybatis配置
mybatis:
    # 搜索指定包别名
    typeAliasesPackage: com.ruoyi.order
    # 配置mapper的扫描,找到所有的mapper.xml映射文件
    mapperLocations: classpath:mapper/**/*.xml

# springdoc配置
springdoc:
gatewayUrl: http://localhost:8080/${spring.application.name}
api-docs:
    # 是否开启接口文档
    enabled: false

</code></pre>
<p>  ruoyi-order示例代码:<br>
<strong>  Order.java</strong></p>
<pre><code class="language-java">package com.ruoyi.order.domain;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class Order {

    private Integer id;

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

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

    /**
   * 订单状态
   */
    private int status;

    /**
   * 数量
   */
    private Integer amount;

    /**
   * 总金额
   */
    private Double totalPrice;

    public Order()
    {
    }

    public Order(Long userId, Long productId, int status, Integer amount)
    {
      this.userId = userId;
      this.productId = productId;
      this.status = status;
      this.amount = amount;
    }
}
</code></pre>
<p><strong>  OrderMapper.java</strong></p>
<pre><code class="language-java">package com.ruoyi.order.mapper;

import com.ruoyi.order.domain.Order;

public interface OrderMapper {
    public void insert(Order order);

    public void updateById(Order order);
}
</code></pre>
<p><strong>  OrderMapper.xml</strong></p>
<pre><code class="language-xml">&lt;?xml version="1.0" encoding="UTF-8" ?&gt;
&lt;!DOCTYPE mapper
      PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
      "http://mybatis.org/dtd/mybatis-3-mapper.dtd"&gt;
&lt;mapper namespace="com.ruoyi.order.mapper.OrderMapper"&gt;

    &lt;resultMap type="com.ruoyi.order.domain.Order" id="OrderResult"&gt;
      &lt;id   property="id"            column="id"                /&gt;
      &lt;result property="userId"          column="user_id"         /&gt;
      &lt;result property="productId"       column="product_id"      /&gt;
      &lt;result property="amount"          column="amount"            /&gt;
      &lt;result property="totalPrice"      column="total_price"       /&gt;
      &lt;result property="status"          column="status"            /&gt;
    &lt;/resultMap&gt;

    &lt;insert id="insert" parameterType="com.ruoyi.order.domain.Order" useGeneratedKeys="true" keyProperty="id"&gt;
      insert into p_order (
      &lt;if test="userId != null and userId != '' "&gt;user_id,&lt;/if&gt;
      &lt;if test="productId != null and productId != '' "&gt;product_id,&lt;/if&gt;
      &lt;if test="amount != null and amount != '' "&gt;amount,&lt;/if&gt;
      &lt;if test="totalPrice != null and totalPrice != '' "&gt;total_price,&lt;/if&gt;
      &lt;if test="status != null and status != ''"&gt;status,&lt;/if&gt;
      add_time
      )values(
      &lt;if test="userId != null and userId != ''"&gt;#{userId},&lt;/if&gt;
      &lt;if test="productId != null and productId != ''"&gt;#{productId},&lt;/if&gt;
      &lt;if test="amount != null and amount != ''"&gt;#{amount},&lt;/if&gt;
      &lt;if test="totalPrice != null and totalPrice != ''"&gt;#{totalPrice},&lt;/if&gt;
      &lt;if test="status != null and status != ''"&gt;#{status},&lt;/if&gt;
      sysdate()
      )
    &lt;/insert&gt;

    &lt;update id="updateById" parameterType="com.ruoyi.order.domain.Order"&gt;
      update p_order
      &lt;set&gt;
            &lt;if test="userId != null and userId != ''"&gt;user_id = #{userId},&lt;/if&gt;
            &lt;if test="productId != null and productId != ''"&gt;product_id = #{productId},&lt;/if&gt;
            &lt;if test="amount != null and amount != ''"&gt;amount = #{amount},&lt;/if&gt;
            &lt;if test="totalPrice != null and totalPrice != ''"&gt;total_price = #{totalPrice},&lt;/if&gt;
            &lt;if test="status != null and status != ''"&gt;status = #{status},&lt;/if&gt;
            last_update_time = sysdate()
      &lt;/set&gt;
      where id = #{id}
    &lt;/update&gt;

&lt;/mapper&gt;
</code></pre>
<p><strong>  OrderService.java</strong></p>
<pre><code class="language-java">package com.ruoyi.order.service;

import com.ruoyi.order.dto.PlaceOrderRequest;

public interface OrderService {
    /**
   * 下单
   *
   * @param placeOrderRequest 订单请求参数
   */
    void placeOrder(PlaceOrderRequest placeOrderRequest);
}
</code></pre>
<p><strong>  OrderServiceImpl.java</strong></p>
<pre><code class="language-java">package com.ruoyi.order.service.impl;

import javax.annotation.Resource;

import com.ruoyi.call.dto.ReduceBalanceRequest;
import com.ruoyi.call.dto.ReduceStockRequest;
import com.ruoyi.call.feign.AccountFeignService;
import com.ruoyi.call.feign.ProductFeignService;
import com.ruoyi.common.core.domain.R;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.baomidou.dynamic.datasource.annotation.DS;
import com.ruoyi.order.domain.Order;
import com.ruoyi.order.dto.PlaceOrderRequest;
import com.ruoyi.order.mapper.OrderMapper;
import com.ruoyi.order.service.OrderService;
import io.seata.core.context.RootContext;
import io.seata.spring.annotation.GlobalTransactional;

@Service
public class OrderServiceImpl implements OrderService {
    private static final Logger log = LoggerFactory.getLogger(OrderServiceImpl.class);

    @Resource
    private OrderMapper orderMapper;

    @Autowired
    private ProductFeignService productFeignService;

    @Autowired
    private AccountFeignService accountFeignService;

    @Override
    @Transactional
    @GlobalTransactional // 重点 第一个开启事务的需要添加seata全局事务注解
    public void placeOrder(PlaceOrderRequest request) {
      log.info("=============ORDER START=================");
      Long userId = request.getUserId();
      Long productId = request.getProductId();
      Integer amount = request.getAmount();
      log.info("收到下单请求,用户:{}, 商品:{},数量:{}", userId, productId, amount);

      log.info("当前 XID: {}", RootContext.getXID());

      Order order = new Order(userId, productId, 0, amount);

      orderMapper.insert(order);
      log.info("订单一阶段生成,等待扣库存付款中");
      // 扣减库存并计算总价
//      Double totalPrice = productService.reduceStock(productId, amount);
//      // 扣减余额
//      accountService.reduceBalance(userId, totalPrice);

      // 扣减库存并计算总价
      R&lt;Double&gt; r = productFeignService.reduceStock(new ReduceStockRequest(productId, amount));
      if (r.getCode() != 200) {
         throw new RuntimeException("扣减库存失败");
      }
      Double totalPrice = r.getData();
      // 扣减余额
      R r1 = accountFeignService.reduceBalance(new ReduceBalanceRequest(userId, totalPrice));
      if (r1.getCode() != 200) {
            throw new RuntimeException("扣减余额失败");
      }
      order.setStatus(1);
      order.setTotalPrice(totalPrice);
      orderMapper.updateById(order);
      log.info("订单已成功下单");
      log.info("=============ORDER END=================");
    }

}
</code></pre>
<p><strong>  OrderController.java</strong></p>
<pre><code class="language-java">package com.ruoyi.order.controller;

import com.ruoyi.common.core.domain.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.order.dto.PlaceOrderRequest;
import com.ruoyi.order.service.OrderService;

@RestController
@RequestMapping("/order")
public class OrderController {
    @Autowired
    private OrderService orderService;

    @PostMapping("/placeOrder")
    public R placeOrder(@Validated @RequestBody PlaceOrderRequest request) {
      orderService.placeOrder(request);
      return R.ok("下单成功");
    }

    @PostMapping("/test1")
    public R test1() {
      // 商品单价10元,库存20个,用户余额50元,模拟一次性购买22个。 期望异常回滚
      orderService.placeOrder(new PlaceOrderRequest(1L, 1L, 22));
      return R.ok("下单成功");
    }

    @PostMapping("/test2")
    public R test2() {
      // 商品单价10元,库存20个,用户余额50元,模拟一次性购买6个。 期望异常回滚
      orderService.placeOrder(new PlaceOrderRequest(1L, 1L, 6));
      return R.ok("下单成功");
    }
}
</code></pre>
<p><strong>  PlaceOrderRequest.java</strong></p>
<pre><code class="language-java">package com.ruoyi.order.dto;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class PlaceOrderRequest {

    private Long userId;

    private Long productId;

    private Integer amount;

    public PlaceOrderRequest() {
    }

    public PlaceOrderRequest(Long userId, Long productId, Integer amount) {
      this.userId = userId;
      this.productId = productId;
      this.amount = amount;
    }
}
</code></pre>
<p>  至此,订单服务就搭建好了。订单服务提供了下单接口,可以模拟正常下单,同时提供了验证库存不足及余额不足的接口,用于验证事务回滚。</p>
<h2 id="测试验证">测试验证</h2>
<p>  使用接口测试工具Postman或Apifox测试接口,注意观察运行日志,以及数据库数据变化,至此分布式事务集成案例全流程完毕。</p>
<h3 id="正常下单">正常下单</h3>
<p>  模拟正常下单,买一个商品 http://localhost:10301/order/placeOrder</p>
<pre><code class="language-json">{
    "userId": 1,
    "productId": 1,
    "amount": 1
}
</code></pre>
<h3 id="库存不足">库存不足</h3>
<p>  模拟库存不足,事务回滚 http://localhost:9201/order/placeOrder</p>
<pre><code class="language-json">{
    "userId": 1,
    "productId": 1,
    "amount": 22
}
</code></pre>
<h3 id="用户余额不足">用户余额不足</h3>
<p>模拟用户余额不足,事务回滚 http://localhost:9201/order/placeOrder</p>
<pre><code>{
    "userId": 1,
    "productId": 1,
    "amount": 6
}
</code></pre>
<h2 id="结语">结语</h2>
<p>  Seata AT模式是微服务场景下最常用的分布式事务解决方案之一,核心优势是对业务无侵入(仅需添加注解),底层基于「两阶段提交+自动补偿」实现数据一致性。AT模式的设计目标是:让开发者像使用本地事务一样使用分布式事务,无需手动编写回滚逻辑。其核心依赖「事务协调器(TC)、资源管理器(RM)、事务管理器(TM)」三大组件,以及「undo_log日志表、全局锁、本地锁」三大核心机制。在AT模式的两阶段提交中,第一阶段执行本地事务时,Seata会自动拦截业务SQL,生成包含数据旧值的undo_log并与业务数据一同提交至数据库;第二阶段若需回滚,框架则通过undo_log反向执行更新操作,完成数据恢复。<br>
  具体使用时只需将@GlobalTransactional注解添加在分布式事务的发起方方法上,Seata便会自动完成全局事务ID的生成、分支事务的注册与协调。使用时具体的注意事项包括:必须使用Seata数据源代理、必须创建undo_log表、所有微服务的tx-service-group、service.vgroupMapping需与Seata Server配置一致等。<br>
  技术探索之路漫漫,由于作者水平有限,文中难免有疏漏或不妥之处,若有不同见解或优化建议,欢迎留言交流指正。</p>
<h2 id="参考资料">参考资料</h2>
<p>https://seata.apache.org/zh-cn/docs/user/quickstart<br>
https://doc.ruoyi.vip/ruoyi-cloud/cloud/seata.html#基本介绍<br>
https://xie.infoq.cn/article/37af299e60562cf625029c29e</p><br><br>
来源:https://www.cnblogs.com/forlp/p/19276340
頁: [1]
查看完整版本: Spring Cloud分布式事务(基于Seata AT模式,集成Nacos)--学习版