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作为注册中心和配置中心。- 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"
- }
- }
复制代码 上传配置至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- #事务分组配置
- 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&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
复制代码 导入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导入数据库中。- -- -------------------------------- 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);
复制代码 至此,Seata-server已经配置完成并且集成了Nacos,事务数据也存储在DB中。可以点击Seata目录下bin/seata-server.bat脚本启动Seata-server。
搭建测试微服务
创建测试库及表
- # 订单数据库信息 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_modified DATETIME 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_modified DATETIME 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_modified DATETIME 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);
复制代码 其中,每个库中的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: ruoyi-account的bootstrap.yml:- # 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:
复制代码 ruoyi-account的bootstrap.yml也是直接拷贝ruoyi-system中的bootstrap.yml,然后修改服务端口,服务名;为了演示方便,关于Seata的配置也直接写在了bootstrap.yml。
ruoyi-account的ruoyi-account-dev.yml:- # 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&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&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
复制代码 ruoyi-account的ruoyi-account-dev.yml也是直接拷贝ruoyi-system中的ruoyi-system-dev.yml,仅修改数据库连接信息,MyBatis扫描的包等,唯一比较重要的一点是动态数据源这里配置了seata:true,开启Seata代理。
ruoyi-account示例代码:
Account.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;
- }
复制代码 AccountMapper.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);
- }
复制代码 AccountMapper.xml- <?xml version="1.0" encoding="UTF-8" ?>
- <!DOCTYPE mapper
- PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
- "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
- <mapper namespace="com.ruoyi.account.mapper.AccountMapper">
- <resultMap type="com.ruoyi.account.domain.Account" id="AccountResult">
- <id property="id" column="id" />
- <result property="balance" column="balance" />
- <result property="lastUpdateTime" column="last_update_time" />
- </resultMap>
- <select id="selectById" parameterType="com.ruoyi.account.domain.Account" resultMap="AccountResult">
- select id, balance, last_update_time
- from account where id = #{userId}
- </select>
- <update id="updateById" parameterType="com.ruoyi.account.domain.Account">
- update account set balance = #{balance}, last_update_time = sysdate() where id = #{id}
- </update>
- </mapper>
复制代码 AccountService.java- package com.ruoyi.account.service;
- public interface AccountService {
- /**
- * 账户扣减
- * @param userId 用户 ID
- * @param price 扣减金额
- */
- void reduceBalance(Long userId, Double price);
- }
复制代码 AccountServiceImpl.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 < 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=================");
- }
- }
复制代码 AccountController.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("下单成功");
- }
- }
复制代码 ReduceBalanceRequest.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;
- }
复制代码 至此,账户服务就搭建好了,这里主要提供一个扣减账户余额的接口。
搭建商品服务
同样,在ruoyi-modules新建一个Module:ruoyi-product。
ruoyi-product的pom.xml: ruoyi-product的bootstrap.yml:- # 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:
复制代码 ruoyi-product的ruoyi-product-dev.yml:- # 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&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&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
复制代码 ruoyi-product示例代码:
Product.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;
- }
复制代码 ProductMapper.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);
- }
复制代码 ProductMapper.xml- <?xml version="1.0" encoding="UTF-8" ?>
- <!DOCTYPE mapper
- PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
- "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
- <mapper namespace="com.ruoyi.product.mapper.ProductMapper">
- <resultMap type="com.ruoyi.product.domain.Product" id="ProductResult">
- <id property="id" column="id" />
- <result property="price" column="price" />
- <result property="stock" column="stock" />
- <result property="lastUpdateTime" column="last_update_time" />
- </resultMap>
- <select id="selectById" parameterType="com.ruoyi.product.domain.Product" resultMap="ProductResult">
- select id, price, stock, last_update_time
- from product where id = #{productId}
- </select>
- <update id="updateById" parameterType="com.ruoyi.product.domain.Product">
- update product set price = #{price}, stock = #{stock}, last_update_time = sysdate() where id = #{id}
- </update>
- </mapper>
复制代码 ProductService.java- package com.ruoyi.product.service;
- public interface ProductService {
- /**
- * 扣减库存
- *
- * @param productId 商品 ID
- * @param amount 扣减数量
- * @return 商品总价
- */
- Double reduceStock(Long productId, Integer amount);
- }
复制代码 ProductServiceImpl.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 < 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;
- }
- }
复制代码 ProductController.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<Double> reduceStock(@Validated @RequestBody ReduceStockRequest request) {
- Double d = productService.reduceStock(request.getProductId(), request.getAmount());
- return R.ok(d);
- }
- }
复制代码 ReduceStockRequest.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;
- }
复制代码 至此,商品服务就搭建好了,这里主要提供一个扣减商品库存的接口。
创建服务调用模块
在ruoyi-modules新建一个Module:ruoyi-call(当你也可以不新建Module,直接把Feign接口写在具调用的服务中,这里新建Module主是为了更好的体现模块之间的低耦合),该模块主要的作用是提供Feign接口,用于订单服务调用商品及账户服务,为了搭建方便,以及减少Maven依赖,pom依赖直接拷贝ruoyi-api-system中的依赖。
ruoyi-call的pom.xml:- <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- <modelVersion>4.0.0</modelVersion>
- <parent>
- <groupId>com.ruoyi</groupId>
- ruoyi-modules</artifactId>
- <version>3.6.6</version>
- </parent>
- ruoyi-call</artifactId>
- <packaging>jar</packaging>
- <name>ruoyi-call</name>
- <url>http://maven.apache.org</url>
- <properties>
- <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
- </properties>
- <dependencies>
- <dependency>
- <groupId>com.ruoyi</groupId>
- ruoyi-common-core</artifactId>
- </dependency>
- <dependency>
- <groupId>org.projectlombok</groupId>
- lombok</artifactId>
- </dependency>
- </dependencies>
- </project>
复制代码 ruoyi-call示例代码:
AccountFeignService.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);
- }
复制代码 ProductFeignService.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<Double> reduceStock(@RequestBody ReduceStockRequest request);
- }
复制代码 ReduceBalanceRequest.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;
- }
复制代码 ReduceStockRequest.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;
- }
复制代码 至此,服务调用模块就创建好了,这里主要提供Feign接口,供订单服务调用。
搭建订单服务
在ruoyi-modules新建一个Module:ruoyi-order,为了搭建方便,以及减少Maven依赖,pom依赖直接拷贝ruoyi-system中的依赖,然后新增Seata的依赖,再就是引入创刚建的服务调用模块。
ruoyi-order的pom.xml: ruoyi-order的bootstrap.yml:- # 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:
复制代码 ruoyi-order的ruoyi-order-dev.yml:- # 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&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&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
复制代码 ruoyi-order示例代码:
Order.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;
- }
- }
复制代码 OrderMapper.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);
- }
复制代码 OrderMapper.xml- <?xml version="1.0" encoding="UTF-8" ?>
- <!DOCTYPE mapper
- PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
- "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
- <mapper namespace="com.ruoyi.order.mapper.OrderMapper">
- <resultMap type="com.ruoyi.order.domain.Order" id="OrderResult">
- <id property="id" column="id" />
- <result property="userId" column="user_id" />
- <result property="productId" column="product_id" />
- <result property="amount" column="amount" />
- <result property="totalPrice" column="total_price" />
- <result property="status" column="status" />
- </resultMap>
- <insert id="insert" parameterType="com.ruoyi.order.domain.Order" useGeneratedKeys="true" keyProperty="id">
- insert into p_order (
- <if test="userId != null and userId != '' ">user_id,</if>
- <if test="productId != null and productId != '' ">product_id,</if>
- <if test="amount != null and amount != '' ">amount,</if>
- <if test="totalPrice != null and totalPrice != '' ">total_price,</if>
- <if test="status != null and status != ''">status,</if>
- add_time
- )values(
- <if test="userId != null and userId != ''">#{userId},</if>
- <if test="productId != null and productId != ''">#{productId},</if>
- <if test="amount != null and amount != ''">#{amount},</if>
- <if test="totalPrice != null and totalPrice != ''">#{totalPrice},</if>
- <if test="status != null and status != ''">#{status},</if>
- sysdate()
- )
- </insert>
- <update id="updateById" parameterType="com.ruoyi.order.domain.Order">
- update p_order
- <set>
- <if test="userId != null and userId != ''">user_id = #{userId},</if>
- <if test="productId != null and productId != ''">product_id = #{productId},</if>
- <if test="amount != null and amount != ''">amount = #{amount},</if>
- <if test="totalPrice != null and totalPrice != ''">total_price = #{totalPrice},</if>
- <if test="status != null and status != ''">status = #{status},</if>
- last_update_time = sysdate()
- </set>
- where id = #{id}
- </update>
- </mapper>
复制代码 OrderService.java- package com.ruoyi.order.service;
- import com.ruoyi.order.dto.PlaceOrderRequest;
- public interface OrderService {
- /**
- * 下单
- *
- * @param placeOrderRequest 订单请求参数
- */
- void placeOrder(PlaceOrderRequest placeOrderRequest);
- }
复制代码 OrderServiceImpl.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<Double> 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=================");
- }
- }
复制代码 OrderController.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("下单成功");
- }
- }
复制代码 PlaceOrderRequest.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;
- }
- }
复制代码 至此,订单服务就搭建好了。订单服务提供了下单接口,可以模拟正常下单,同时提供了验证库存不足及余额不足的接口,用于验证事务回滚。
测试验证
使用接口测试工具Postman或Apifox测试接口,注意观察运行日志,以及数据库数据变化,至此分布式事务集成案例全流程完毕。
正常下单
模拟正常下单,买一个商品 http://localhost:10301/order/placeOrder- {
- "userId": 1,
- "productId": 1,
- "amount": 1
- }
复制代码 库存不足
模拟库存不足,事务回滚 http://localhost:9201/order/placeOrder- {
- "userId": 1,
- "productId": 1,
- "amount": 22
- }
复制代码 用户余额不足
模拟用户余额不足,事务回滚 http://localhost:9201/order/placeOrder- {
- "userId": 1,
- "productId": 1,
- "amount": 6
- }
复制代码 结语
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
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |