找回密码
 立即注册
首页 业界区 业界 Spring Cloud分布式事务(基于Seata AT模式,集成Nacos ...

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

米嘉怡 2025-11-27 12:10:00
Spring Cloud分布式事务快速上手(基于Seata AT模式,集成Nacos)--学习版

前言

  对于从未接触过Seata的同学来说,想要快速上手Seata还是需要花费比较长的时间,因为本身微服务开发中环境的搭建、以及各种配置都已经很繁琐了,然后再集成Seata,Seata又有许多配置,对于每个微服务来说,针对Seata又有一些配置,要搞清楚各种配置之间的关系,对于像我这样的小白来说,着实不是一件容易的事。但Seata作为分布式事务的关键解决方案,在微服务架构中起着至关重要的作用。接下来,我将结合自身小白学习踩坑的过程,为大家介绍Seata的实操步骤,帮助大家少走弯路。
依赖的相关环境及组件

  为了方便像我这样的小白快速上手,我只能踩着巨人的肩膀前进,本文中相关demo基本参考ruoyi-cloud官方文档中的示例(能参考别人的代码千万别自己动手写),只做了部分改造,方便验证;同时,微服务环境框架也是直接用的ruoyi-cloud,感谢每一位开源前辈无私无畏的奉献。
组件版本    ruoyi-cloud        v3.6.6        Seata        1.4.0        Nacos        2.5.0    部署Seata-server(集成Nacos)

下载Seata-server

  可以从GitHub仓库https://github.com/apache/incubator-seata/releases下载各版本的Seata-server(直达链接:Seata-server各Releases版本)。Windows下载解压后(.zip),直接点击bin/seata-server.bat就可以启动。
配置Seata-server,集成Nacos

  使用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作为注册中心和配置中心。
  1. registry {
  2.   # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  3.   type = "nacos"
  4.   loadBalance = "RandomLoadBalance"
  5.   loadBalanceVirtualNodes = 10
  6.   nacos {
  7.     application = "seata-server"
  8.     serverAddr = "127.0.0.1:8848"
  9.     group = "SEATA_GROUP"
  10.     namespace = ""
  11.     cluster = "default"
  12.     username = ""
  13.     password = ""
  14.   }
  15.   eureka {
  16.     serviceUrl = "http://localhost:8761/eureka"
  17.     application = "default"
  18.     weight = "1"
  19.   }
  20.   redis {
  21.     serverAddr = "localhost:6379"
  22.     db = 0
  23.     password = ""
  24.     cluster = "default"
  25.     timeout = 0
  26.   }
  27.   zk {
  28.     cluster = "default"
  29.     serverAddr = "127.0.0.1:2181"
  30.     sessionTimeout = 6000
  31.     connectTimeout = 2000
  32.     username = ""
  33.     password = ""
  34.   }
  35.   consul {
  36.     cluster = "default"
  37.     serverAddr = "127.0.0.1:8500"
  38.   }
  39.   etcd3 {
  40.     cluster = "default"
  41.     serverAddr = "http://localhost:2379"
  42.   }
  43.   sofa {
  44.     serverAddr = "127.0.0.1:9603"
  45.     application = "default"
  46.     region = "DEFAULT_ZONE"
  47.     datacenter = "DefaultDataCenter"
  48.     cluster = "default"
  49.     group = "SEATA_GROUP"
  50.     addressWaitTime = "3000"
  51.   }
  52.   file {
  53.     name = "file.conf"
  54.   }
  55. }
  56. config {
  57.   # file、nacos 、apollo、zk、consul、etcd3
  58.   type = "nacos"
  59.   nacos {
  60.     serverAddr = "127.0.0.1:8848"
  61.     namespace = ""
  62.     group = "SEATA_GROUP"
  63.     username = ""
  64.     password = ""
  65.   }
  66.   consul {
  67.     serverAddr = "127.0.0.1:8500"
  68.   }
  69.   apollo {
  70.     appId = "seata-server"
  71.     apolloMeta = "http://192.168.1.204:8801"
  72.     namespace = "application"
  73.     apolloAccesskeySecret = ""
  74.   }
  75.   zk {
  76.     serverAddr = "127.0.0.1:2181"
  77.     sessionTimeout = 6000
  78.     connectTimeout = 2000
  79.     username = ""
  80.     password = ""
  81.   }
  82.   etcd3 {
  83.     serverAddr = "http://localhost:2379"
  84.   }
  85.   file {
  86.     name = "file.conf"
  87.   }
  88. }
复制代码
上传配置至Nacos配置中心

  在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):
  如下修改为db,分布式事务的核心数据存储至数据库
  store.mode=db
  store.lock.mode=db
  store.session.mode=db
  以及配置数据库连接信息
  store.db.driverClassName
  store.db.url
  store.db.user       
  store.db.password
  增加事务分组配置,每个服务一个,一般都采用将key 值设置为服务端的服务名,有多少个微服务就添加多少行。
  #后边会搭建账户、商品、订单三个测试微服务,这配置每个微服务的事务分组
  service.vgroupMapping.ruoyi-account-group=default
  service.vgroupMapping.ruoyi-order-group=default
  service.vgroupMapping.ruoyi-product-group=default
  1. #事务分组配置
  2. service.vgroupMapping.ruoyi-account-group=default
  3. service.vgroupMapping.ruoyi-order-group=default
  4. service.vgroupMapping.ruoyi-product-group=default
  5. #For details about configuration items, see https://seata.io/zh-cn/docs/user/configurations.html
  6. #Transport configuration, for client and server
  7. transport.type=TCP
  8. transport.server=NIO
  9. transport.heartbeat=true
  10. transport.enableTmClientBatchSendRequest=false
  11. transport.enableRmClientBatchSendRequest=true
  12. transport.enableTcServerBatchSendResponse=false
  13. transport.rpcRmRequestTimeout=30000
  14. transport.rpcTmRequestTimeout=30000
  15. transport.rpcTcRequestTimeout=30000
  16. transport.threadFactory.bossThreadPrefix=NettyBoss
  17. transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
  18. transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
  19. transport.threadFactory.shareBossWorker=false
  20. transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
  21. transport.threadFactory.clientSelectorThreadSize=1
  22. transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
  23. transport.threadFactory.bossThreadSize=1
  24. transport.threadFactory.workerThreadSize=default
  25. transport.shutdown.wait=3
  26. transport.serialization=seata
  27. transport.compressor=none
  28. #Transaction routing rules configuration, only for the client
  29. service.vgroupMapping.default_tx_group=default
  30. #If you use a registry, you can ignore it
  31. service.default.grouplist=127.0.0.1:8091
  32. service.enableDegrade=false
  33. service.disableGlobalTransaction=false
  34. #Transaction rule configuration, only for the client
  35. client.rm.asyncCommitBufferLimit=10000
  36. client.rm.lock.retryInterval=10
  37. client.rm.lock.retryTimes=30
  38. client.rm.lock.retryPolicyBranchRollbackOnConflict=true
  39. client.rm.reportRetryCount=5
  40. client.rm.tableMetaCheckEnable=true
  41. client.rm.tableMetaCheckerInterval=60000
  42. client.rm.sqlParserType=druid
  43. client.rm.reportSuccessEnable=false
  44. client.rm.sagaBranchRegisterEnable=false
  45. client.rm.sagaJsonParser=fastjson
  46. client.rm.tccActionInterceptorOrder=-2147482648
  47. client.tm.commitRetryCount=5
  48. client.tm.rollbackRetryCount=5
  49. client.tm.defaultGlobalTransactionTimeout=60000
  50. client.tm.degradeCheck=false
  51. client.tm.degradeCheckAllowTimes=10
  52. client.tm.degradeCheckPeriod=2000
  53. client.tm.interceptorOrder=-2147482648
  54. client.undo.dataValidation=true
  55. client.undo.logSerialization=jackson
  56. client.undo.onlyCareUpdateColumns=true
  57. server.undo.logSaveDays=7
  58. server.undo.logDeletePeriod=86400000
  59. client.undo.logTable=undo_log
  60. client.undo.compress.enable=true
  61. client.undo.compress.type=zip
  62. client.undo.compress.threshold=64k
  63. #For TCC transaction mode
  64. tcc.fence.logTableName=tcc_fence_log
  65. tcc.fence.cleanPeriod=1h
  66. #Log rule configuration, for client and server
  67. log.exceptionRate=100
  68. #Transaction storage configuration, only for the server. The file, db, and redis configuration values are optional.
  69. # 修改为db,分布式事务的核心数据存储至数据库
  70. store.mode=db
  71. store.lock.mode=db
  72. store.session.mode=db
  73. #Used for password encryption
  74. store.publicKey=
  75. #If `store.mode,store.lock.mode,store.session.mode` are not equal to `file`, you can remove the configuration block.
  76. store.file.dir=file_store/data
  77. store.file.maxBranchSessionSize=16384
  78. store.file.maxGlobalSessionSize=512
  79. store.file.fileWriteBufferCacheSize=16384
  80. store.file.flushDiskMode=async
  81. store.file.sessionReloadReadSize=100
  82. #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.
  83. store.db.datasource=druid
  84. store.db.dbType=mysql
  85. store.db.driverClassName=com.mysql.jdbc.Driver
  86. # 配置数据库连接信息
  87. store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true
  88. store.db.user=username
  89. store.db.password=password
  90. store.db.minConn=5
  91. store.db.maxConn=30
  92. store.db.globalTable=global_table
  93. store.db.branchTable=branch_table
  94. store.db.distributedLockTable=distributed_lock
  95. store.db.queryLimit=100
  96. store.db.lockTable=lock_table
  97. store.db.maxWait=5000
  98. #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.
  99. store.redis.mode=single
  100. store.redis.single.host=127.0.0.1
  101. store.redis.single.port=6379
  102. store.redis.sentinel.masterName=
  103. store.redis.sentinel.sentinelHosts=
  104. store.redis.sentinel.sentinelPassword=
  105. store.redis.maxConn=10
  106. store.redis.minConn=1
  107. store.redis.maxTotal=100
  108. store.redis.database=0
  109. store.redis.password=
  110. store.redis.queryLimit=100
  111. #Transaction rule configuration, only for the server
  112. server.recovery.committingRetryPeriod=1000
  113. server.recovery.asynCommittingRetryPeriod=1000
  114. server.recovery.rollbackingRetryPeriod=1000
  115. server.recovery.timeoutRetryPeriod=1000
  116. server.maxCommitRetryTimeout=-1
  117. server.maxRollbackRetryTimeout=-1
  118. server.rollbackFailedUnlockEnable=false
  119. server.distributedLockExpireTime=10000
  120. server.xaerNotaRetryTimeout=60000
  121. server.session.branchAsyncQueueSize=5000
  122. server.session.enableBranchAsyncRemove=false
  123. server.enableParallelRequestHandle=false
  124. #Metrics configuration, only for the server
  125. metrics.enabled=false
  126. metrics.registryType=compact
  127. metrics.exporterList=prometheus
  128. metrics.exporterPrometheusPort=9898
复制代码
导入Seata-server所需SQL

  前面我们已经配置了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导入数据库中。
  1. -- -------------------------------- The script used when storeMode is 'db' --------------------------------
  2. -- the table to store GlobalSession data
  3. CREATE TABLE IF NOT EXISTS `global_table`
  4. (
  5.     `xid`                       VARCHAR(128) NOT NULL,
  6.     `transaction_id`            BIGINT,
  7.     `status`                    TINYINT      NOT NULL,
  8.     `application_id`            VARCHAR(32),
  9.     `transaction_service_group` VARCHAR(32),
  10.     `transaction_name`          VARCHAR(128),
  11.     `timeout`                   INT,
  12.     `begin_time`                BIGINT,
  13.     `application_data`          VARCHAR(2000),
  14.     `gmt_create`                DATETIME,
  15.     `gmt_modified`              DATETIME,
  16.     PRIMARY KEY (`xid`),
  17.     KEY `idx_status_gmt_modified` (`status` , `gmt_modified`),
  18.     KEY `idx_transaction_id` (`transaction_id`)
  19. ) ENGINE = InnoDB
  20.   DEFAULT CHARSET = utf8mb4;
  21. -- the table to store BranchSession data
  22. CREATE TABLE IF NOT EXISTS `branch_table`
  23. (
  24.     `branch_id`         BIGINT       NOT NULL,
  25.     `xid`               VARCHAR(128) NOT NULL,
  26.     `transaction_id`    BIGINT,
  27.     `resource_group_id` VARCHAR(32),
  28.     `resource_id`       VARCHAR(256),
  29.     `branch_type`       VARCHAR(8),
  30.     `status`            TINYINT,
  31.     `client_id`         VARCHAR(64),
  32.     `application_data`  VARCHAR(2000),
  33.     `gmt_create`        DATETIME(6),
  34.     `gmt_modified`      DATETIME(6),
  35.     PRIMARY KEY (`branch_id`),
  36.     KEY `idx_xid` (`xid`)
  37. ) ENGINE = InnoDB
  38.   DEFAULT CHARSET = utf8mb4;
  39. -- the table to store lock data
  40. CREATE TABLE IF NOT EXISTS `lock_table`
  41. (
  42.     `row_key`        VARCHAR(128) NOT NULL,
  43.     `xid`            VARCHAR(128),
  44.     `transaction_id` BIGINT,
  45.     `branch_id`      BIGINT       NOT NULL,
  46.     `resource_id`    VARCHAR(256),
  47.     `table_name`     VARCHAR(32),
  48.     `pk`             VARCHAR(36),
  49.     `status`         TINYINT      NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',
  50.     `gmt_create`     DATETIME,
  51.     `gmt_modified`   DATETIME,
  52.     PRIMARY KEY (`row_key`),
  53.     KEY `idx_status` (`status`),
  54.     KEY `idx_branch_id` (`branch_id`),
  55.     KEY `idx_xid` (`xid`)
  56. ) ENGINE = InnoDB
  57.   DEFAULT CHARSET = utf8mb4;
  58. CREATE TABLE IF NOT EXISTS `distributed_lock`
  59. (
  60.     `lock_key`       CHAR(20) NOT NULL,
  61.     `lock_value`     VARCHAR(20) NOT NULL,
  62.     `expire`         BIGINT,
  63.     primary key (`lock_key`)
  64. ) ENGINE = InnoDB
  65.   DEFAULT CHARSET = utf8mb4;
  66. INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);
  67. INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);
  68. INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);
  69. INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);
复制代码
  至此,Seata-server已经配置完成并且集成了Nacos,事务数据也存储在DB中。可以点击Seata目录下bin/seata-server.bat脚本启动Seata-server。
1.png

搭建测试微服务

创建测试库及表
  1. # 订单数据库信息 seata_order
  2. DROP DATABASE IF EXISTS seata_order;
  3. CREATE DATABASE seata_order;
  4. DROP TABLE IF EXISTS seata_order.p_order;
  5. CREATE TABLE seata_order.p_order
  6. (
  7.     id               INT(11) NOT NULL AUTO_INCREMENT,
  8.     user_id          INT(11) DEFAULT NULL,
  9.     product_id       INT(11) DEFAULT NULL,
  10.     amount           INT(11) DEFAULT NULL,
  11.     total_price      DOUBLE       DEFAULT NULL,
  12.     status           VARCHAR(100) DEFAULT NULL,
  13.     add_time         DATETIME     DEFAULT CURRENT_TIMESTAMP,
  14.     last_update_time DATETIME     DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  15.     PRIMARY KEY (id)
  16. ) ENGINE = InnoDB
  17.   AUTO_INCREMENT = 1
  18.   DEFAULT CHARSET = utf8mb4;
  19. DROP TABLE IF EXISTS seata_order.undo_log;
  20. CREATE TABLE seata_order.undo_log
  21. (
  22.     id            BIGINT(20) NOT NULL AUTO_INCREMENT,
  23.     branch_id     BIGINT(20) NOT NULL,
  24.     xid           VARCHAR(100) NOT NULL,
  25.     context       VARCHAR(128) NOT NULL,
  26.     rollback_info LONGBLOB     NOT NULL,
  27.     log_status    INT(11) NOT NULL,
  28.     log_created   DATETIME     NOT NULL,
  29.     log_modified  DATETIME     NOT NULL,
  30.     PRIMARY KEY (id),
  31.     UNIQUE KEY ux_undo_log (xid, branch_id)
  32. ) ENGINE = InnoDB
  33.   AUTO_INCREMENT = 1
  34.   DEFAULT CHARSET = utf8mb4;
  35.   
  36. # 产品数据库信息 seata_product
  37. DROP DATABASE IF EXISTS seata_product;
  38. CREATE DATABASE seata_product;
  39. DROP TABLE IF EXISTS seata_product.product;
  40. CREATE TABLE seata_product.product
  41. (
  42.     id               INT(11) NOT NULL AUTO_INCREMENT,
  43.     price            DOUBLE   DEFAULT NULL,
  44.     stock            INT(11) DEFAULT NULL,
  45.     last_update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  46.     PRIMARY KEY (id)
  47. ) ENGINE = InnoDB
  48.   AUTO_INCREMENT = 1
  49.   DEFAULT CHARSET = utf8mb4;
  50. DROP TABLE IF EXISTS seata_product.undo_log;
  51. CREATE TABLE seata_product.undo_log
  52. (
  53.     id            BIGINT(20) NOT NULL AUTO_INCREMENT,
  54.     branch_id     BIGINT(20) NOT NULL,
  55.     xid           VARCHAR(100) NOT NULL,
  56.     context       VARCHAR(128) NOT NULL,
  57.     rollback_info LONGBLOB     NOT NULL,
  58.     log_status    INT(11) NOT NULL,
  59.     log_created   DATETIME     NOT NULL,
  60.     log_modified  DATETIME     NOT NULL,
  61.     PRIMARY KEY (id),
  62.     UNIQUE KEY ux_undo_log (xid, branch_id)
  63. ) ENGINE = InnoDB
  64.   AUTO_INCREMENT = 1
  65.   DEFAULT CHARSET = utf8mb4;
  66. INSERT INTO seata_product.product (id, price, stock)
  67. VALUES (1, 10, 20);
  68. # 账户数据库信息 seata_account
  69. DROP DATABASE IF EXISTS seata_account;
  70. CREATE DATABASE seata_account;
  71. DROP TABLE IF EXISTS seata_account.account;
  72. CREATE TABLE seata_account.account
  73. (
  74.     id               INT(11) NOT NULL AUTO_INCREMENT,
  75.     balance          DOUBLE   DEFAULT NULL,
  76.     last_update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  77.     PRIMARY KEY (id)
  78. ) ENGINE = InnoDB
  79.   AUTO_INCREMENT = 1
  80.   DEFAULT CHARSET = utf8mb4;
  81. DROP TABLE IF EXISTS seata_account.undo_log;
  82. CREATE TABLE seata_account.undo_log
  83. (
  84.     id            BIGINT(20) NOT NULL AUTO_INCREMENT,
  85.     branch_id     BIGINT(20) NOT NULL,
  86.     xid           VARCHAR(100) NOT NULL,
  87.     context       VARCHAR(128) NOT NULL,
  88.     rollback_info LONGBLOB     NOT NULL,
  89.     log_status    INT(11) NOT NULL,
  90.     log_created   DATETIME     NOT NULL,
  91.     log_modified  DATETIME     NOT NULL,
  92.     PRIMARY KEY (id),
  93.     UNIQUE KEY ux_undo_log (xid, branch_id)
  94. ) ENGINE = InnoDB
  95.   AUTO_INCREMENT = 1
  96.   DEFAULT CHARSET = utf8mb4;
  97. INSERT INTO seata_account.account (id, balance)
  98. VALUES (1, 50);
复制代码
  其中,每个库中的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记录。
搭建测试服务

搭建账户服务

  在ruoyi-modules新建一个Module:ruoyi-account,为了搭建方便,以及减少Maven依赖,pom依赖直接拷贝ruoyi-system中的依赖,然后新增Seata的依赖就可以了。
  ruoyi-account的pom.xml:
  1. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  2.          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  3.     <modelVersion>4.0.0</modelVersion>
  4.     <parent>
  5.         <groupId>com.ruoyi</groupId>
  6.         ruoyi-modules</artifactId>
  7.         <version>3.6.6</version>
  8.     </parent>
  9.     ruoyi-account</artifactId>
  10.     <packaging>jar</packaging>
  11.     <name>ruoyi-account</name>
  12.     <url>http://maven.apache.org</url>
  13.     <properties>
  14.         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  15.     </properties>
  16.     <dependencies>
  17.         
  18.         <dependency>
  19.             <groupId>com.alibaba.cloud</groupId>
  20.             spring-cloud-starter-alibaba-nacos-discovery</artifactId>
  21.         </dependency>
  22.         
  23.         <dependency>
  24.             <groupId>com.alibaba.cloud</groupId>
  25.             spring-cloud-starter-alibaba-nacos-config</artifactId>
  26.         </dependency>
  27.         
  28.         <dependency>
  29.             <groupId>com.alibaba.cloud</groupId>
  30.             spring-cloud-starter-alibaba-sentinel</artifactId>
  31.         </dependency>
  32.         
  33.         <dependency>
  34.             <groupId>org.springframework.boot</groupId>
  35.             spring-boot-starter-actuator</artifactId>
  36.         </dependency>
  37.         
  38.         <dependency>
  39.             <groupId>com.mysql</groupId>
  40.             mysql-connector-j</artifactId>
  41.         </dependency>
  42.         
  43.         <dependency>
  44.             <groupId>com.ruoyi</groupId>
  45.             ruoyi-common-datasource</artifactId>
  46.         </dependency>
  47.         
  48.         <dependency>
  49.             <groupId>com.ruoyi</groupId>
  50.             ruoyi-common-datascope</artifactId>
  51.         </dependency>
  52.         
  53.         <dependency>
  54.             <groupId>com.ruoyi</groupId>
  55.             ruoyi-common-log</artifactId>
  56.         </dependency>
  57.         
  58.         <dependency>
  59.             <groupId>com.ruoyi</groupId>
  60.             ruoyi-common-swagger</artifactId>
  61.         </dependency>
  62.         
  63.         <dependency>
  64.             <groupId>com.alibaba.cloud</groupId>
  65.             spring-cloud-starter-alibaba-seata</artifactId>
  66.         </dependency>
  67.         <dependency>
  68.             <groupId>org.projectlombok</groupId>
  69.             lombok</artifactId>
  70.         </dependency>
  71.     </dependencies>
  72.     <build>
  73.         <finalName>${project.artifactId}</finalName>
  74.         <plugins>
  75.             <plugin>
  76.                 <groupId>org.springframework.boot</groupId>
  77.                 spring-boot-maven-plugin</artifactId>
  78.                 <executions>
  79.                     <execution>
  80.                         <goals>
  81.                             <goal>repackage</goal>
  82.                         </goals>
  83.                     </execution>
  84.                 </executions>
  85.             </plugin>
  86.         </plugins>
  87.     </build>
  88. </project>
复制代码
  ruoyi-account的bootstrap.yml:
  1. # Tomcat
  2. server:
  3.   port: 10300
  4. # Spring
  5. spring:
  6.   application:
  7.     # 应用名称
  8.     name: ruoyi-account
  9.   profiles:
  10.     # 环境配置
  11.     active: dev
  12.   cloud:
  13.     nacos:
  14.       discovery:
  15.         # 服务注册地址
  16.         server-addr: 127.0.0.1:8848
  17.       config:
  18.         # 配置中心地址
  19.         server-addr: 127.0.0.1:8848
  20.         # 配置文件格式
  21.         file-extension: yml
  22.         # 共享配置
  23.         shared-configs:
  24.           - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
  25. seata:
  26.   enabled: true
  27.   # Seata 应用编号,默认为 ${spring.application.name}
  28.   application-id: ${spring.application.name}
  29.   # Seata 事务组编号,用于 TC 集群名
  30.   tx-service-group: ${spring.application.name}-group
  31.   # 关闭自动代理
  32.   enable-auto-data-source-proxy: false
  33.   # 服务配置项
  34.   service:
  35.     # 虚拟组和分组的映射
  36.     vgroup-mapping:
  37.       ruoyi-account-group: default
  38.   config:
  39.     type: nacos
  40.     nacos:
  41.       serverAddr: 127.0.0.1:8848
  42.       group: SEATA_GROUP
  43.       namespace:
  44.       dataId: seataServer.properties
  45.   registry:
  46.     type: nacos
  47.     nacos:
  48.       application: seata-server
  49.       server-addr: 127.0.0.1:8848
  50.       namespace:
复制代码
  ruoyi-account的bootstrap.yml也是直接拷贝ruoyi-system中的bootstrap.yml,然后修改服务端口,服务名;为了演示方便,关于Seata的配置也直接写在了bootstrap.yml。
  ruoyi-account的ruoyi-account-dev.yml:
  1. # spring配置
  2. spring:
  3.   redis:
  4.     host: localhost
  5.     port: 6379
  6.     password:
  7.   datasource:
  8.     druid:
  9.       stat-view-servlet:
  10.         enabled: true
  11.         loginUsername: ruoyi
  12.         loginPassword: 123456
  13.     dynamic:
  14.       druid:
  15.         initial-size: 5
  16.         min-idle: 5
  17.         maxActive: 20
  18.         maxWait: 60000
  19.         connectTimeout: 30000
  20.         socketTimeout: 60000
  21.         timeBetweenEvictionRunsMillis: 60000
  22.         minEvictableIdleTimeMillis: 300000
  23.         validationQuery: SELECT 1 FROM DUAL
  24.         testWhileIdle: true
  25.         testOnBorrow: false
  26.         testOnReturn: false
  27.         poolPreparedStatements: true
  28.         maxPoolPreparedStatementPerConnectionSize: 20
  29.         filters: stat,slf4j
  30.         connectionProperties: druid.stat.mergeSql\=true;druid.stat.slowSqlMillis\=5000
  31.       datasource:
  32.           # 主库数据源
  33.           master:
  34.             driver-class-name: com.mysql.cj.jdbc.Driver
  35.             url: jdbc:mysql://localhost:3306/seata_account?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
  36.             username: root
  37.             password: root123
  38.           # 从库数据源
  39.           # slave:
  40.             # username:
  41.             # password:
  42.             # url:
  43.             # driver-class-name:
  44.       seata: true #开启seata代理,开启后默认每个数据源都代理,如果某个不需要代理可单独关闭
  45. # mybatis配置
  46. mybatis:
  47.     # 搜索指定包别名
  48.     typeAliasesPackage: com.ruoyi.account
  49.     # 配置mapper的扫描,找到所有的mapper.xml映射文件
  50.     mapperLocations: classpath:mapper/**/*.xml
  51. # springdoc配置
  52. springdoc:
  53.   gatewayUrl: http://localhost:8080/${spring.application.name}
  54.   api-docs:
  55.     # 是否开启接口文档
  56.     enabled: false
复制代码
  ruoyi-account的ruoyi-account-dev.yml也是直接拷贝ruoyi-system中的ruoyi-system-dev.yml,仅修改数据库连接信息,MyBatis扫描的包等,唯一比较重要的一点是动态数据源这里配置了seata:true,开启Seata代理。
  ruoyi-account示例代码:
  Account.java
  1. package com.ruoyi.account.domain;
  2. import lombok.Getter;
  3. import lombok.Setter;
  4. import java.util.Date;
  5. @Getter
  6. @Setter
  7. public class Account {
  8.     private Long id;
  9.     /**
  10.      * 余额
  11.      */
  12.     private Double balance;
  13.     private Date lastUpdateTime;
  14. }
复制代码
  AccountMapper.java
  1. package com.ruoyi.account.mapper;
  2. import com.ruoyi.account.domain.Account;
  3. public interface AccountMapper {
  4.     public Account selectById(Long userId);
  5.     public void updateById(Account account);
  6. }
复制代码
  AccountMapper.xml
  1. <?xml version="1.0" encoding="UTF-8" ?>
  2. <!DOCTYPE mapper
  3.         PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  4.         "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  5. <mapper namespace="com.ruoyi.account.mapper.AccountMapper">
  6.     <resultMap type="com.ruoyi.account.domain.Account" id="AccountResult">
  7.         <id     property="id"              column="id"                />
  8.         <result property="balance"         column="balance"           />
  9.         <result property="lastUpdateTime"  column="last_update_time"  />
  10.     </resultMap>
  11.     <select id="selectById" parameterType="com.ruoyi.account.domain.Account" resultMap="AccountResult">
  12.         select id, balance, last_update_time
  13.         from account where id = #{userId}
  14.     </select>
  15.     <update id="updateById" parameterType="com.ruoyi.account.domain.Account">
  16.         update account set balance = #{balance}, last_update_time = sysdate() where id = #{id}
  17.     </update>
  18. </mapper>
复制代码
  AccountService.java
  1. package com.ruoyi.account.service;
  2. public interface AccountService {
  3.     /**
  4.      * 账户扣减
  5.      * @param userId 用户 ID
  6.      * @param price 扣减金额
  7.      */
  8.     void reduceBalance(Long userId, Double price);
  9. }
复制代码
  AccountServiceImpl.java
  1. package com.ruoyi.account.service.impl;
  2. import javax.annotation.Resource;
  3. import org.slf4j.Logger;
  4. import org.slf4j.LoggerFactory;
  5. import org.springframework.stereotype.Service;
  6. import org.springframework.transaction.annotation.Propagation;
  7. import org.springframework.transaction.annotation.Transactional;
  8. import com.baomidou.dynamic.datasource.annotation.DS;
  9. import com.ruoyi.account.domain.Account;
  10. import com.ruoyi.account.mapper.AccountMapper;
  11. import com.ruoyi.account.service.AccountService;
  12. import io.seata.core.context.RootContext;
  13. @Service
  14. public class AccountServiceImpl implements AccountService
  15. {
  16.     private static final Logger log = LoggerFactory.getLogger(AccountServiceImpl.class);
  17.    
  18.     @Resource
  19.     private AccountMapper accountMapper;
  20.     /**
  21.      * 事务传播特性设置为 REQUIRES_NEW 开启新的事务 重要!!!!一定要使用REQUIRES_NEW
  22.      * 在若依的示例中,事务的传播特性必须设置REQUIRES_NEW,但经本人测试,多服务调用事务
  23.      * 设置成默认的REQUIRED也能回滚成功
  24.      */
  25.     @Override
  26.     @Transactional
  27.     public void reduceBalance(Long userId, Double price)
  28.     {
  29.         log.info("=============ACCOUNT START=================");
  30.         log.info("当前 XID: {}", RootContext.getXID());
  31.         Account account = accountMapper.selectById(userId);
  32.         Double balance = account.getBalance();
  33.         log.info("下单用户{}余额为 {},商品总价为{}", userId, balance, price);
  34.         if (balance < price)
  35.         {
  36.             log.warn("用户 {} 余额不足,当前余额:{}", userId, balance);
  37.             throw new RuntimeException("余额不足");
  38.         }
  39.         log.info("开始扣减用户 {} 余额", userId);
  40.         double currentBalance = account.getBalance() - price;
  41.         account.setBalance(currentBalance);
  42.         accountMapper.updateById(account);
  43.         log.info("扣减用户 {} 余额成功,扣减后用户账户余额为{}", userId, currentBalance);
  44.         log.info("=============ACCOUNT END=================");
  45.     }
  46. }
复制代码
  AccountController.java
  1. package com.ruoyi.account.controller;
  2. import com.ruoyi.account.dto.ReduceBalanceRequest;
  3. import com.ruoyi.account.service.AccountService;
  4. import com.ruoyi.common.core.domain.R;
  5. import org.springframework.beans.factory.annotation.Autowired;
  6. import org.springframework.validation.annotation.Validated;
  7. import org.springframework.web.bind.annotation.PostMapping;
  8. import org.springframework.web.bind.annotation.RequestBody;
  9. import org.springframework.web.bind.annotation.RequestMapping;
  10. import org.springframework.web.bind.annotation.RestController;
  11. @RestController
  12. @RequestMapping("/account")
  13. public class AccountController {
  14.     @Autowired
  15.     private AccountService accountService;
  16.     @PostMapping("/reduceBalance")
  17.     public R reduceBalance(@Validated @RequestBody ReduceBalanceRequest request) {
  18.         accountService.reduceBalance(request.getUserId(), request.getPrice());
  19.         return R.ok("下单成功");
  20.     }
  21. }
复制代码
  ReduceBalanceRequest.java
  1. package com.ruoyi.account.dto;
  2. import lombok.AllArgsConstructor;
  3. import lombok.Getter;
  4. import lombok.NoArgsConstructor;
  5. import lombok.Setter;
  6. @Getter
  7. @Setter
  8. @NoArgsConstructor
  9. @AllArgsConstructor
  10. public class ReduceBalanceRequest {
  11.     private Long userId;
  12.     private Double price;
  13. }
复制代码
  至此,账户服务就搭建好了,这里主要提供一个扣减账户余额的接口。
搭建商品服务

  同样,在ruoyi-modules新建一个Module:ruoyi-product。
  ruoyi-product的pom.xml:
  1. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  2.          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  3.     <modelVersion>4.0.0</modelVersion>
  4.     <parent>
  5.         <groupId>com.ruoyi</groupId>
  6.         ruoyi-modules</artifactId>
  7.         <version>3.6.6</version>
  8.     </parent>
  9.     ruoyi-product</artifactId>
  10.     <packaging>jar</packaging>
  11.     <name>ruoyi-product</name>
  12.     <url>http://maven.apache.org</url>
  13.     <properties>
  14.         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  15.     </properties>
  16.     <dependencies>
  17.         
  18.         <dependency>
  19.             <groupId>com.alibaba.cloud</groupId>
  20.             spring-cloud-starter-alibaba-nacos-discovery</artifactId>
  21.         </dependency>
  22.         
  23.         <dependency>
  24.             <groupId>com.alibaba.cloud</groupId>
  25.             spring-cloud-starter-alibaba-nacos-config</artifactId>
  26.         </dependency>
  27.         
  28.         <dependency>
  29.             <groupId>com.alibaba.cloud</groupId>
  30.             spring-cloud-starter-alibaba-sentinel</artifactId>
  31.         </dependency>
  32.         
  33.         <dependency>
  34.             <groupId>org.springframework.boot</groupId>
  35.             spring-boot-starter-actuator</artifactId>
  36.         </dependency>
  37.         
  38.         <dependency>
  39.             <groupId>com.mysql</groupId>
  40.             mysql-connector-j</artifactId>
  41.         </dependency>
  42.         
  43.         <dependency>
  44.             <groupId>com.ruoyi</groupId>
  45.             ruoyi-common-datasource</artifactId>
  46.         </dependency>
  47.         
  48.         <dependency>
  49.             <groupId>com.ruoyi</groupId>
  50.             ruoyi-common-datascope</artifactId>
  51.         </dependency>
  52.         
  53.         <dependency>
  54.             <groupId>com.ruoyi</groupId>
  55.             ruoyi-common-log</artifactId>
  56.         </dependency>
  57.         
  58.         <dependency>
  59.             <groupId>com.ruoyi</groupId>
  60.             ruoyi-common-swagger</artifactId>
  61.         </dependency>
  62.         
  63.         <dependency>
  64.             <groupId>com.alibaba.cloud</groupId>
  65.             spring-cloud-starter-alibaba-seata</artifactId>
  66.         </dependency>
  67.         <dependency>
  68.             <groupId>org.projectlombok</groupId>
  69.             lombok</artifactId>
  70.         </dependency>
  71.     </dependencies>
  72.     <build>
  73.         <finalName>${project.artifactId}</finalName>
  74.         <plugins>
  75.             <plugin>
  76.                 <groupId>org.springframework.boot</groupId>
  77.                 spring-boot-maven-plugin</artifactId>
  78.                 <executions>
  79.                     <execution>
  80.                         <goals>
  81.                             <goal>repackage</goal>
  82.                         </goals>
  83.                     </execution>
  84.                 </executions>
  85.             </plugin>
  86.         </plugins>
  87.     </build>
  88. </project>
复制代码
  ruoyi-product的bootstrap.yml:
  1. # Tomcat
  2. server:
  3.   port: 10302
  4. # Spring
  5. spring:
  6.   application:
  7.     # 应用名称
  8.     name: ruoyi-product
  9.   profiles:
  10.     # 环境配置
  11.     active: dev
  12.   cloud:
  13.     nacos:
  14.       discovery:
  15.         # 服务注册地址
  16.         server-addr: 127.0.0.1:8848
  17.       config:
  18.         # 配置中心地址
  19.         server-addr: 127.0.0.1:8848
  20.         # 配置文件格式
  21.         file-extension: yml
  22.         # 共享配置
  23.         shared-configs:
  24.           - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
  25. seata:
  26.   enabled: true
  27.   # Seata 应用编号,默认为 ${spring.application.name}
  28.   application-id: ${spring.application.name}
  29.   # Seata 事务组编号,用于 TC 集群名
  30.   tx-service-group: ${spring.application.name}-group
  31.   # 关闭自动代理
  32.   enable-auto-data-source-proxy: false
  33.   # 服务配置项
  34.   service:
  35.     # 虚拟组和分组的映射
  36.     vgroup-mapping:
  37.       ruoyi-product-group: default
  38.   config:
  39.     type: nacos
  40.     nacos:
  41.       serverAddr: 127.0.0.1:8848
  42.       group: SEATA_GROUP
  43.       namespace:
  44.       dataId: seataServer.properties
  45.   registry:
  46.     type: nacos
  47.     nacos:
  48.       application: seata-server
  49.       server-addr: 127.0.0.1:8848
  50.       namespace:
复制代码
  ruoyi-product的ruoyi-product-dev.yml:
  1. # spring配置
  2. spring:
  3.   redis:
  4.     host: localhost
  5.     port: 6379
  6.     password:
  7.   datasource:
  8.     druid:
  9.       stat-view-servlet:
  10.         enabled: true
  11.         loginUsername: ruoyi
  12.         loginPassword: 123456
  13.     dynamic:
  14.       druid:
  15.         initial-size: 5
  16.         min-idle: 5
  17.         maxActive: 20
  18.         maxWait: 60000
  19.         connectTimeout: 30000
  20.         socketTimeout: 60000
  21.         timeBetweenEvictionRunsMillis: 60000
  22.         minEvictableIdleTimeMillis: 300000
  23.         validationQuery: SELECT 1 FROM DUAL
  24.         testWhileIdle: true
  25.         testOnBorrow: false
  26.         testOnReturn: false
  27.         poolPreparedStatements: true
  28.         maxPoolPreparedStatementPerConnectionSize: 20
  29.         filters: stat,slf4j
  30.         connectionProperties: druid.stat.mergeSql\=true;druid.stat.slowSqlMillis\=5000
  31.       datasource:
  32.           # 主库数据源
  33.           master:
  34.             driver-class-name: com.mysql.cj.jdbc.Driver
  35.             url: jdbc:mysql://localhost:3306/seata_product?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
  36.             username: root
  37.             password: root123
  38.           # 从库数据源
  39.           # slave:
  40.             # username:
  41.             # password:
  42.             # url:
  43.             # driver-class-name:
  44.       seata: true
  45. # mybatis配置
  46. mybatis:
  47.     # 搜索指定包别名
  48.     typeAliasesPackage: com.ruoyi.product
  49.     # 配置mapper的扫描,找到所有的mapper.xml映射文件
  50.     mapperLocations: classpath:mapper/**/*.xml
  51. # springdoc配置
  52. springdoc:
  53.   gatewayUrl: http://localhost:8080/${spring.application.name}
  54.   api-docs:
  55.     # 是否开启接口文档
  56.     enabled: false
复制代码
  ruoyi-product示例代码:
  Product.java
  1. package com.ruoyi.product.domain;
  2. import lombok.Getter;
  3. import lombok.Setter;
  4. import java.util.Date;
  5. @Getter
  6. @Setter
  7. public class Product {
  8.     private Integer id;
  9.     /**
  10.      * 价格
  11.      */
  12.     private Double price;
  13.     /**
  14.      * 库存
  15.      */
  16.     private Integer stock;
  17.     private Date lastUpdateTime;
  18. }
复制代码
  ProductMapper.java
  1. package com.ruoyi.product.mapper;
  2. import com.ruoyi.product.domain.Product;
  3. public interface ProductMapper {
  4.     public Product selectById(Long productId);
  5.     public void updateById(Product product);
  6. }
复制代码
  ProductMapper.xml
  1. <?xml version="1.0" encoding="UTF-8" ?>
  2. <!DOCTYPE mapper
  3.         PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  4.         "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  5. <mapper namespace="com.ruoyi.product.mapper.ProductMapper">
  6.     <resultMap type="com.ruoyi.product.domain.Product" id="ProductResult">
  7.         <id     property="id"              column="id"                />
  8.         <result property="price"           column="price"             />
  9.         <result property="stock"           column="stock"             />
  10.         <result property="lastUpdateTime"  column="last_update_time"  />
  11.     </resultMap>
  12.     <select id="selectById" parameterType="com.ruoyi.product.domain.Product" resultMap="ProductResult">
  13.         select id, price, stock, last_update_time
  14.         from product where id = #{productId}
  15.     </select>
  16.     <update id="updateById" parameterType="com.ruoyi.product.domain.Product">
  17.         update product set price = #{price}, stock = #{stock}, last_update_time = sysdate() where id = #{id}
  18.     </update>
  19. </mapper>
复制代码
  ProductService.java
  1. package com.ruoyi.product.service;
  2. public interface ProductService {
  3.     /**
  4.      * 扣减库存
  5.      *
  6.      * @param productId 商品 ID
  7.      * @param amount 扣减数量
  8.      * @return 商品总价
  9.      */
  10.     Double reduceStock(Long productId, Integer amount);
  11. }
复制代码
  ProductServiceImpl.java
  1. package com.ruoyi.product.service.impl;
  2. import javax.annotation.Resource;
  3. import org.slf4j.Logger;
  4. import org.slf4j.LoggerFactory;
  5. import org.springframework.stereotype.Service;
  6. import org.springframework.transaction.annotation.Propagation;
  7. import org.springframework.transaction.annotation.Transactional;
  8. import com.baomidou.dynamic.datasource.annotation.DS;
  9. import com.ruoyi.product.domain.Product;
  10. import com.ruoyi.product.mapper.ProductMapper;
  11. import com.ruoyi.product.service.ProductService;
  12. import io.seata.core.context.RootContext;
  13. @Service
  14. public class ProductServiceImpl implements ProductService {
  15.     private static final Logger log = LoggerFactory.getLogger(ProductServiceImpl.class);
  16.     @Resource
  17.     private ProductMapper productMapper;
  18.     /**
  19.      * 事务传播特性设置为 REQUIRES_NEW 开启新的事务 重要!!!!一定要使用REQUIRES_NEW
  20.      * 在若依的示例中,事务的传播特性必须设置REQUIRES_NEW,但经本人测试,多服务调用事务
  21.      * 设置成默认的REQUIRED也能回滚成功
  22.      */
  23.     @Transactional
  24.     @Override
  25.     public Double reduceStock(Long productId, Integer amount)
  26.     {
  27.         log.info("=============PRODUCT START=================");
  28.         log.info("当前 XID: {}", RootContext.getXID());
  29.         // 检查库存
  30.         Product product = productMapper.selectById(productId);
  31.         Integer stock = product.getStock();
  32.         log.info("商品编号为 {} 的库存为{},订单商品数量为{}", productId, stock, amount);
  33.         if (stock < amount)
  34.         {
  35.             log.warn("商品编号为{} 库存不足,当前库存:{}", productId, stock);
  36.             throw new RuntimeException("库存不足");
  37.         }
  38.         log.info("开始扣减商品编号为 {} 库存,单价商品价格为{}", productId, product.getPrice());
  39.         // 扣减库存
  40.         int currentStock = stock - amount;
  41.         product.setStock(currentStock);
  42.         productMapper.updateById(product);
  43.         double totalPrice = product.getPrice() * amount;
  44.         log.info("扣减商品编号为 {} 库存成功,扣减后库存为{}, {} 件商品总价为 {} ", productId, currentStock, amount, totalPrice);
  45.         log.info("=============PRODUCT END=================");
  46.         return totalPrice;
  47.     }
  48. }
复制代码
  ProductController.java
  1. package com.ruoyi.product.controller;
  2. import com.ruoyi.common.core.domain.R;
  3. import com.ruoyi.product.dto.ReduceStockRequest;
  4. import com.ruoyi.product.service.ProductService;
  5. import org.springframework.beans.factory.annotation.Autowired;
  6. import org.springframework.validation.annotation.Validated;
  7. import org.springframework.web.bind.annotation.PostMapping;
  8. import org.springframework.web.bind.annotation.RequestBody;
  9. import org.springframework.web.bind.annotation.RequestMapping;
  10. import org.springframework.web.bind.annotation.RestController;
  11. @RestController
  12. @RequestMapping("/product")
  13. public class ProductController {
  14.     @Autowired
  15.     private ProductService productService;
  16.     @PostMapping("/reduceStock")
  17.     public R<Double> reduceStock(@Validated @RequestBody ReduceStockRequest request) {
  18.         Double d = productService.reduceStock(request.getProductId(), request.getAmount());
  19.         return R.ok(d);
  20.     }
  21. }
复制代码
  ReduceStockRequest.java
  1. package com.ruoyi.product.dto;
  2. import lombok.AllArgsConstructor;
  3. import lombok.Getter;
  4. import lombok.NoArgsConstructor;
  5. import lombok.Setter;
  6. @Getter
  7. @Setter
  8. @NoArgsConstructor
  9. @AllArgsConstructor
  10. public class ReduceStockRequest {
  11.     private Long productId;
  12.     private Integer amount;
  13. }
复制代码
  至此,商品服务就搭建好了,这里主要提供一个扣减商品库存的接口。
创建服务调用模块

  在ruoyi-modules新建一个Module:ruoyi-call(当你也可以不新建Module,直接把Feign接口写在具调用的服务中,这里新建Module主是为了更好的体现模块之间的低耦合),该模块主要的作用是提供Feign接口,用于订单服务调用商品及账户服务,为了搭建方便,以及减少Maven依赖,pom依赖直接拷贝ruoyi-api-system中的依赖。
  ruoyi-call的pom.xml:
  1. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  2.          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  3.     <modelVersion>4.0.0</modelVersion>
  4.     <parent>
  5.         <groupId>com.ruoyi</groupId>
  6.         ruoyi-modules</artifactId>
  7.         <version>3.6.6</version>
  8.     </parent>
  9.     ruoyi-call</artifactId>
  10.     <packaging>jar</packaging>
  11.     <name>ruoyi-call</name>
  12.     <url>http://maven.apache.org</url>
  13.     <properties>
  14.         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  15.     </properties>
  16.     <dependencies>
  17.         <dependency>
  18.             <groupId>com.ruoyi</groupId>
  19.             ruoyi-common-core</artifactId>
  20.         </dependency>
  21.         <dependency>
  22.             <groupId>org.projectlombok</groupId>
  23.             lombok</artifactId>
  24.         </dependency>
  25.     </dependencies>
  26. </project>
复制代码
  ruoyi-call示例代码:
  AccountFeignService.java
  1. package com.ruoyi.call.feign;
  2. import com.ruoyi.call.dto.ReduceBalanceRequest;
  3. import com.ruoyi.common.core.domain.R;
  4. import org.springframework.cloud.openfeign.FeignClient;
  5. import org.springframework.web.bind.annotation.PostMapping;
  6. import org.springframework.web.bind.annotation.RequestBody;
  7. @FeignClient(name = "ruoyi-account")
  8. public interface AccountFeignService {
  9.     @PostMapping("/account/reduceBalance")
  10.     R reduceBalance(@RequestBody ReduceBalanceRequest request);
  11. }
复制代码
  ProductFeignService.java
  1. package com.ruoyi.call.feign;
  2. import com.ruoyi.call.dto.ReduceStockRequest;
  3. import com.ruoyi.common.core.domain.R;
  4. import org.springframework.cloud.openfeign.FeignClient;
  5. import org.springframework.web.bind.annotation.PostMapping;
  6. import org.springframework.web.bind.annotation.RequestBody;
  7. @FeignClient(name = "ruoyi-product")
  8. public interface ProductFeignService {
  9.     @PostMapping("/product/reduceStock")
  10.     R<Double> reduceStock(@RequestBody ReduceStockRequest request);
  11. }
复制代码
  ReduceBalanceRequest.java
  1. package com.ruoyi.call.dto;
  2. import lombok.AllArgsConstructor;
  3. import lombok.Getter;
  4. import lombok.NoArgsConstructor;
  5. import lombok.Setter;
  6. @Getter
  7. @Setter
  8. @NoArgsConstructor
  9. @AllArgsConstructor
  10. public class ReduceBalanceRequest {
  11.     private Long userId;
  12.     private Double price;
  13. }
复制代码
  ReduceStockRequest.java
  1. package com.ruoyi.call.dto;
  2. import lombok.AllArgsConstructor;
  3. import lombok.Getter;
  4. import lombok.NoArgsConstructor;
  5. import lombok.Setter;
  6. @Getter
  7. @Setter
  8. @NoArgsConstructor
  9. @AllArgsConstructor
  10. public class ReduceStockRequest {
  11.     private Long productId;
  12.     private Integer amount;
  13. }
复制代码
  至此,服务调用模块就创建好了,这里主要提供Feign接口,供订单服务调用。
搭建订单服务

  在ruoyi-modules新建一个Module:ruoyi-order,为了搭建方便,以及减少Maven依赖,pom依赖直接拷贝ruoyi-system中的依赖,然后新增Seata的依赖,再就是引入创刚建的服务调用模块。
  ruoyi-order的pom.xml:
  1. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  2.          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  3.     <modelVersion>4.0.0</modelVersion>
  4.     <parent>
  5.         <groupId>com.ruoyi</groupId>
  6.         ruoyi-modules</artifactId>
  7.         <version>3.6.6</version>
  8.     </parent>
  9.     ruoyi-order</artifactId>
  10.     <packaging>jar</packaging>
  11.     <name>ruoyi-order</name>
  12.     <url>http://maven.apache.org</url>
  13.     <properties>
  14.         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  15.     </properties>
  16.     <dependencies>
  17.         
  18.         <dependency>
  19.             <groupId>com.alibaba.cloud</groupId>
  20.             spring-cloud-starter-alibaba-nacos-discovery</artifactId>
  21.         </dependency>
  22.         
  23.         <dependency>
  24.             <groupId>com.alibaba.cloud</groupId>
  25.             spring-cloud-starter-alibaba-nacos-config</artifactId>
  26.         </dependency>
  27.         
  28.         <dependency>
  29.             <groupId>com.alibaba.cloud</groupId>
  30.             spring-cloud-starter-alibaba-sentinel</artifactId>
  31.         </dependency>
  32.         
  33.         <dependency>
  34.             <groupId>org.springframework.boot</groupId>
  35.             spring-boot-starter-actuator</artifactId>
  36.         </dependency>
  37.         
  38.         <dependency>
  39.             <groupId>com.mysql</groupId>
  40.             mysql-connector-j</artifactId>
  41.         </dependency>
  42.         
  43.         <dependency>
  44.             <groupId>com.ruoyi</groupId>
  45.             ruoyi-common-datasource</artifactId>
  46.         </dependency>
  47.         
  48.         <dependency>
  49.             <groupId>com.ruoyi</groupId>
  50.             ruoyi-common-datascope</artifactId>
  51.         </dependency>
  52.         
  53.         <dependency>
  54.             <groupId>com.ruoyi</groupId>
  55.             ruoyi-common-log</artifactId>
  56.         </dependency>
  57.         
  58.         <dependency>
  59.             <groupId>com.ruoyi</groupId>
  60.             ruoyi-common-swagger</artifactId>
  61.         </dependency>
  62.         
  63.         <dependency>
  64.             <groupId>com.alibaba.cloud</groupId>
  65.             spring-cloud-starter-alibaba-seata</artifactId>
  66.         </dependency>
  67.         <dependency>
  68.             <groupId>com.ruoyi</groupId>
  69.             ruoyi-call</artifactId>
  70.             <version>3.6.6</version>
  71.         </dependency>
  72.     </dependencies>
  73.     <build>
  74.         <finalName>${project.artifactId}</finalName>
  75.         <plugins>
  76.             <plugin>
  77.                 <groupId>org.springframework.boot</groupId>
  78.                 spring-boot-maven-plugin</artifactId>
  79.                 <executions>
  80.                     <execution>
  81.                         <goals>
  82.                             <goal>repackage</goal>
  83.                         </goals>
  84.                     </execution>
  85.                 </executions>
  86.             </plugin>
  87.         </plugins>
  88.     </build>
  89. </project>
复制代码
  ruoyi-order的bootstrap.yml:
  1. # Tomcat
  2. server:
  3.   port: 10301
  4. # Spring
  5. spring:
  6.   application:
  7.     # 应用名称
  8.     name: ruoyi-order
  9.   profiles:
  10.     # 环境配置
  11.     active: dev
  12.   cloud:
  13.     nacos:
  14.       discovery:
  15.         # 服务注册地址
  16.         server-addr: 127.0.0.1:8848
  17.       config:
  18.         # 配置中心地址
  19.         server-addr: 127.0.0.1:8848
  20.         # 配置文件格式
  21.         file-extension: yml
  22.         # 共享配置
  23.         shared-configs:
  24.           - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
  25. seata:
  26.   enabled: true
  27.   # Seata 应用编号,默认为 ${spring.application.name}
  28.   application-id: ${spring.application.name}
  29.   # Seata 事务组编号,用于 TC 集群名
  30.   tx-service-group: ${spring.application.name}-group
  31.   # 关闭自动代理
  32.   enable-auto-data-source-proxy: false
  33.   # 服务配置项
  34.   service:
  35.     # 虚拟组和分组的映射
  36.     vgroup-mapping:
  37.       ruoyi-order-group: default
  38.   config:
  39.     type: nacos
  40.     nacos:
  41.       serverAddr: 127.0.0.1:8848
  42.       group: SEATA_GROUP
  43.       namespace:
  44.       dataId: seataServer.properties
  45.   registry:
  46.     type: nacos
  47.     nacos:
  48.       application: seata-server
  49.       server-addr: 127.0.0.1:8848
  50.       namespace:
复制代码
  ruoyi-order的ruoyi-order-dev.yml:
  1. # spring配置
  2. spring:
  3.   redis:
  4.     host: localhost
  5.     port: 6379
  6.     password:
  7.   datasource:
  8.     druid:
  9.       stat-view-servlet:
  10.         enabled: true
  11.         loginUsername: ruoyi
  12.         loginPassword: 123456
  13.     dynamic:
  14.       druid:
  15.         initial-size: 5
  16.         min-idle: 5
  17.         maxActive: 20
  18.         maxWait: 60000
  19.         connectTimeout: 30000
  20.         socketTimeout: 60000
  21.         timeBetweenEvictionRunsMillis: 60000
  22.         minEvictableIdleTimeMillis: 300000
  23.         validationQuery: SELECT 1 FROM DUAL
  24.         testWhileIdle: true
  25.         testOnBorrow: false
  26.         testOnReturn: false
  27.         poolPreparedStatements: true
  28.         maxPoolPreparedStatementPerConnectionSize: 20
  29.         filters: stat,slf4j
  30.         connectionProperties: druid.stat.mergeSql\=true;druid.stat.slowSqlMillis\=5000
  31.       datasource:
  32.           # 主库数据源
  33.           master:
  34.             driver-class-name: com.mysql.cj.jdbc.Driver
  35.             url: jdbc:mysql://localhost:3306/seata_order?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
  36.             username: root
  37.             password: root123
  38.           # 从库数据源
  39.           # slave:
  40.             # username:
  41.             # password:
  42.             # url:
  43.             # driver-class-name:
  44.       seata: true
  45. # mybatis配置
  46. mybatis:
  47.     # 搜索指定包别名
  48.     typeAliasesPackage: com.ruoyi.order
  49.     # 配置mapper的扫描,找到所有的mapper.xml映射文件
  50.     mapperLocations: classpath:mapper/**/*.xml
  51. # springdoc配置
  52. springdoc:
  53.   gatewayUrl: http://localhost:8080/${spring.application.name}
  54.   api-docs:
  55.     # 是否开启接口文档
  56.     enabled: false
复制代码
  ruoyi-order示例代码:
  Order.java
  1. package com.ruoyi.order.domain;
  2. import lombok.Getter;
  3. import lombok.Setter;
  4. @Getter
  5. @Setter
  6. public class Order {
  7.     private Integer id;
  8.     /**
  9.      * 用户ID
  10.      */
  11.     private Long userId;
  12.     /**
  13.      * 商品ID
  14.      */
  15.     private Long productId;
  16.     /**
  17.      * 订单状态
  18.      */
  19.     private int status;
  20.     /**
  21.      * 数量
  22.      */
  23.     private Integer amount;
  24.     /**
  25.      * 总金额
  26.      */
  27.     private Double totalPrice;
  28.     public Order()
  29.     {
  30.     }
  31.     public Order(Long userId, Long productId, int status, Integer amount)
  32.     {
  33.         this.userId = userId;
  34.         this.productId = productId;
  35.         this.status = status;
  36.         this.amount = amount;
  37.     }
  38. }
复制代码
  OrderMapper.java
  1. package com.ruoyi.order.mapper;
  2. import com.ruoyi.order.domain.Order;
  3. public interface OrderMapper {
  4.     public void insert(Order order);
  5.     public void updateById(Order order);
  6. }
复制代码
  OrderMapper.xml
  1. <?xml version="1.0" encoding="UTF-8" ?>
  2. <!DOCTYPE mapper
  3.         PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  4.         "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  5. <mapper namespace="com.ruoyi.order.mapper.OrderMapper">
  6.     <resultMap type="com.ruoyi.order.domain.Order" id="OrderResult">
  7.         <id     property="id"              column="id"                />
  8.         <result property="userId"          column="user_id"           />
  9.         <result property="productId"       column="product_id"        />
  10.         <result property="amount"          column="amount"            />
  11.         <result property="totalPrice"      column="total_price"       />
  12.         <result property="status"          column="status"            />
  13.     </resultMap>
  14.     <insert id="insert" parameterType="com.ruoyi.order.domain.Order" useGeneratedKeys="true" keyProperty="id">
  15.         insert into p_order (
  16.         <if test="userId != null and userId != '' ">user_id,</if>
  17.         <if test="productId != null and productId != '' ">product_id,</if>
  18.         <if test="amount != null and amount != '' ">amount,</if>
  19.         <if test="totalPrice != null and totalPrice != '' ">total_price,</if>
  20.         <if test="status != null and status != ''">status,</if>
  21.         add_time
  22.         )values(
  23.         <if test="userId != null and userId != ''">#{userId},</if>
  24.         <if test="productId != null and productId != ''">#{productId},</if>
  25.         <if test="amount != null and amount != ''">#{amount},</if>
  26.         <if test="totalPrice != null and totalPrice != ''">#{totalPrice},</if>
  27.         <if test="status != null and status != ''">#{status},</if>
  28.         sysdate()
  29.         )
  30.     </insert>
  31.     <update id="updateById" parameterType="com.ruoyi.order.domain.Order">
  32.         update p_order
  33.         <set>
  34.             <if test="userId != null and userId != ''">user_id = #{userId},</if>
  35.             <if test="productId != null and productId != ''">product_id = #{productId},</if>
  36.             <if test="amount != null and amount != ''">amount = #{amount},</if>
  37.             <if test="totalPrice != null and totalPrice != ''">total_price = #{totalPrice},</if>
  38.             <if test="status != null and status != ''">status = #{status},</if>
  39.             last_update_time = sysdate()
  40.         </set>
  41.         where id = #{id}
  42.     </update>
  43. </mapper>
复制代码
  OrderService.java
  1. package com.ruoyi.order.service;
  2. import com.ruoyi.order.dto.PlaceOrderRequest;
  3. public interface OrderService {
  4.     /**
  5.      * 下单
  6.      *
  7.      * @param placeOrderRequest 订单请求参数
  8.      */
  9.     void placeOrder(PlaceOrderRequest placeOrderRequest);
  10. }
复制代码
  OrderServiceImpl.java
  1. package com.ruoyi.order.service.impl;
  2. import javax.annotation.Resource;
  3. import com.ruoyi.call.dto.ReduceBalanceRequest;
  4. import com.ruoyi.call.dto.ReduceStockRequest;
  5. import com.ruoyi.call.feign.AccountFeignService;
  6. import com.ruoyi.call.feign.ProductFeignService;
  7. import com.ruoyi.common.core.domain.R;
  8. import org.slf4j.Logger;
  9. import org.slf4j.LoggerFactory;
  10. import org.springframework.beans.factory.annotation.Autowired;
  11. import org.springframework.stereotype.Service;
  12. import org.springframework.transaction.annotation.Transactional;
  13. import com.baomidou.dynamic.datasource.annotation.DS;
  14. import com.ruoyi.order.domain.Order;
  15. import com.ruoyi.order.dto.PlaceOrderRequest;
  16. import com.ruoyi.order.mapper.OrderMapper;
  17. import com.ruoyi.order.service.OrderService;
  18. import io.seata.core.context.RootContext;
  19. import io.seata.spring.annotation.GlobalTransactional;
  20. @Service
  21. public class OrderServiceImpl implements OrderService {
  22.     private static final Logger log = LoggerFactory.getLogger(OrderServiceImpl.class);
  23.     @Resource
  24.     private OrderMapper orderMapper;
  25.     @Autowired
  26.     private ProductFeignService productFeignService;
  27.     @Autowired
  28.     private AccountFeignService accountFeignService;
  29.     @Override
  30.     @Transactional
  31.     @GlobalTransactional // 重点 第一个开启事务的需要添加seata全局事务注解
  32.     public void placeOrder(PlaceOrderRequest request) {
  33.         log.info("=============ORDER START=================");
  34.         Long userId = request.getUserId();
  35.         Long productId = request.getProductId();
  36.         Integer amount = request.getAmount();
  37.         log.info("收到下单请求,用户:{}, 商品:{},数量:{}", userId, productId, amount);
  38.         log.info("当前 XID: {}", RootContext.getXID());
  39.         Order order = new Order(userId, productId, 0, amount);
  40.         orderMapper.insert(order);
  41.         log.info("订单一阶段生成,等待扣库存付款中");
  42.         // 扣减库存并计算总价
  43. //        Double totalPrice = productService.reduceStock(productId, amount);
  44. //        // 扣减余额
  45. //        accountService.reduceBalance(userId, totalPrice);
  46.         // 扣减库存并计算总价
  47.         R<Double> r = productFeignService.reduceStock(new ReduceStockRequest(productId, amount));
  48.         if (r.getCode() != 200) {
  49.            throw new RuntimeException("扣减库存失败");
  50.         }
  51.         Double totalPrice = r.getData();
  52.         // 扣减余额
  53.         R r1 = accountFeignService.reduceBalance(new ReduceBalanceRequest(userId, totalPrice));
  54.         if (r1.getCode() != 200) {
  55.             throw new RuntimeException("扣减余额失败");
  56.         }
  57.         order.setStatus(1);
  58.         order.setTotalPrice(totalPrice);
  59.         orderMapper.updateById(order);
  60.         log.info("订单已成功下单");
  61.         log.info("=============ORDER END=================");
  62.     }
  63. }
复制代码
  OrderController.java
  1. package com.ruoyi.order.controller;
  2. import com.ruoyi.common.core.domain.R;
  3. import org.springframework.beans.factory.annotation.Autowired;
  4. import org.springframework.validation.annotation.Validated;
  5. import org.springframework.web.bind.annotation.PostMapping;
  6. import org.springframework.web.bind.annotation.RequestBody;
  7. import org.springframework.web.bind.annotation.RequestMapping;
  8. import org.springframework.web.bind.annotation.RestController;
  9. import com.ruoyi.order.dto.PlaceOrderRequest;
  10. import com.ruoyi.order.service.OrderService;
  11. @RestController
  12. @RequestMapping("/order")
  13. public class OrderController {
  14.     @Autowired
  15.     private OrderService orderService;
  16.     @PostMapping("/placeOrder")
  17.     public R placeOrder(@Validated @RequestBody PlaceOrderRequest request) {
  18.         orderService.placeOrder(request);
  19.         return R.ok("下单成功");
  20.     }
  21.     @PostMapping("/test1")
  22.     public R test1() {
  23.         // 商品单价10元,库存20个,用户余额50元,模拟一次性购买22个。 期望异常回滚
  24.         orderService.placeOrder(new PlaceOrderRequest(1L, 1L, 22));
  25.         return R.ok("下单成功");
  26.     }
  27.     @PostMapping("/test2")
  28.     public R test2() {
  29.         // 商品单价10元,库存20个,用户余额50元,模拟一次性购买6个。 期望异常回滚
  30.         orderService.placeOrder(new PlaceOrderRequest(1L, 1L, 6));
  31.         return R.ok("下单成功");
  32.     }
  33. }
复制代码
  PlaceOrderRequest.java
  1. package com.ruoyi.order.dto;
  2. import lombok.Getter;
  3. import lombok.Setter;
  4. @Getter
  5. @Setter
  6. public class PlaceOrderRequest {
  7.     private Long userId;
  8.     private Long productId;
  9.     private Integer amount;
  10.     public PlaceOrderRequest() {
  11.     }
  12.     public PlaceOrderRequest(Long userId, Long productId, Integer amount) {
  13.         this.userId = userId;
  14.         this.productId = productId;
  15.         this.amount = amount;
  16.     }
  17. }
复制代码
  至此,订单服务就搭建好了。订单服务提供了下单接口,可以模拟正常下单,同时提供了验证库存不足及余额不足的接口,用于验证事务回滚。
测试验证

  使用接口测试工具Postman或Apifox测试接口,注意观察运行日志,以及数据库数据变化,至此分布式事务集成案例全流程完毕。
正常下单

  模拟正常下单,买一个商品 http://localhost:10301/order/placeOrder
  1. {
  2.     "userId": 1,
  3.     "productId": 1,
  4.     "amount": 1
  5. }
复制代码
库存不足

  模拟库存不足,事务回滚 http://localhost:9201/order/placeOrder
  1. {
  2.     "userId": 1,
  3.     "productId": 1,
  4.     "amount": 22
  5. }
复制代码
用户余额不足

模拟用户余额不足,事务回滚 http://localhost:9201/order/placeOrder
  1. {
  2.     "userId": 1,
  3.     "productId": 1,
  4.     "amount": 6
  5. }
复制代码
结语

  Seata AT模式是微服务场景下最常用的分布式事务解决方案之一,核心优势是对业务无侵入(仅需添加注解),底层基于「两阶段提交+自动补偿」实现数据一致性。AT模式的设计目标是:让开发者像使用本地事务一样使用分布式事务,无需手动编写回滚逻辑。其核心依赖「事务协调器(TC)、资源管理器(RM)、事务管理器(TM)」三大组件,以及「undo_log日志表、全局锁、本地锁」三大核心机制。在AT模式的两阶段提交中,第一阶段执行本地事务时,Seata会自动拦截业务SQL,生成包含数据旧值的undo_log并与业务数据一同提交至数据库;第二阶段若需回滚,框架则通过undo_log反向执行更新操作,完成数据恢复。
  具体使用时只需将@GlobalTransactional注解添加在分布式事务的发起方方法上,Seata便会自动完成全局事务ID的生成、分支事务的注册与协调。使用时具体的注意事项包括:必须使用Seata数据源代理、必须创建undo_log表、所有微服务的tx-service-group、service.vgroupMapping需与Seata Server配置一致等。
  技术探索之路漫漫,由于作者水平有限,文中难免有疏漏或不妥之处,若有不同见解或优化建议,欢迎留言交流指正。
参考资料

https://seata.apache.org/zh-cn/docs/user/quickstart
https://doc.ruoyi.vip/ruoyi-cloud/cloud/seata.html#基本介绍
https://xie.infoq.cn/article/37af299e60562cf625029c29e

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册