【java_wxid项目】【第七章】【Spring Cloud Alibaba Seata集成】

简介: 主项目链接:https://gitee.com/java_wxid/java_wxid项目架构及博文总结:

主项目链接:https://gitee.com/java_wxid/java_wxid

项目架构及博文总结:


  • 点击:【使用Spring Boot快速构建应用】
  • 点击:【使用Spring Cloud Open Feign基于动态代理动态构造请求实现与其他系统进行交互】
  • 点击:【使用Spring Cloud Hystrix实现服务容错、熔断、降级、监控】
  • 点击:【使用Spring Cloud Ribbon以库的方式集成到服务的消费方实现客户端负载均衡】
  • 点击:【使用Spring Cloud Gateway作为API网关服务进行请求拦截、服务分发、降级、限流】
  • 点击:【使用Spring Cloud Security Oauth2作为微服务统一认证中心实现用户认证和授权访问】
  • 点击:【使用Spring Cloud Stream作为消息驱动用于动态的切换中间件】
  • 点击:【使用Spring Cloud Skywalking基于字节码注入通过探针方式进行链路追踪、分布式追踪、性能指标分析、应用和服务依赖分析】
  • 点击:【使用Spring Cloud Alibaba Nacos实现服务注册/发现/续约/剔除/下线、心跳检测、服务配置管理、基于长轮训机制实现配置动态变更】
  • 点击:【使用Spring Cloud Alibaba Seata作为对项目代码无入侵的分布式事务解决方案】
  • 点击:【使用Spring Cloud Alibaba Sentinel实现高可用流量防护】
  • 点击:【使用Apache ShardingSphere作为关系型数据库中间件实现分库分表、读写分离】
  • 点击:【使用Apache Mybatis作为持久层框架用于定制化SQL、存储过程以及高级映射】
  • 点击:【使用Redis作为高性能分布式缓存数据库】
  • 点击:【使用ElasticSearch全文搜索】
  • 点击:【使用MongoDB非关系型数据库】
  • 点击:【使用xxl-job作为分布式任务调度平台】
  • 点击:【使用Elasticsearch + Logstash + Kibana作为日志收集系统】
  • 点击:【使用Apifox作为API文档、API调试、API Mock、API自动化测试】
  • 点击:【使用Apache Spark作为基于内存计算的大数据分析引擎用于批处理、交互式查询】
  • 点击:【使用ETL工具将数据源抽取到HDFS作为高可靠、高吞吐量的分布式文件系统存储,通过Hive清洗、处理和计算原始数据,Hive清洗处理后的结果,将存入Hbase,海量数据随机查询场景从HBase查询数据】
  • 点击:【使用领域驱动DDD设计和设计模式进行开发】
  • 点击:【使用Netty基于Java NIO封装的高性能的网络通信框架】
  • 点击:【使用k8s、docker、docker-compose、宝塔面板进行环境搭建和部署】
  • 点击:【使用Vue渐进式JavaScript框架作为适用场景丰富的Web前端框架】
  • 点击:【分享人才筛选、工作分配、高效办公、项目推动等团队管理经验】


项目模块:


前期规划,实现部分

java_wxid   
├── demo                                                            // 演示模块
│     └── 模块名称:apache-mybatis-demo模块                            //Apache Mybatis集成(已实现并有博文总结)
│     └── 模块名称:apache-shardingsphere-demo模块                     //Apache ShardingSphere集成(已实现并有博文总结)
│     └── 模块名称:design-demo模块                                    //设计模式实战落地(已实现并有博文总结)
│     └── 模块名称:elasticsearch-demo模块                             //ElasticSearch集成(已实现并有博文总结)
│     └── 模块名称:mongodb-demo模块                                   //MongoDB集成(已实现并有博文总结)
│     └── 模块名称:redis-demo模块                                     //Redis集成(已实现并有博文总结)
│     └── 模块名称:spring-boot-demo模块                               //Spring Boot快速构建应用(已实现并有博文总结)
│     └── 模块名称:spring-cloud-alibaba-nacos-demo模块                //Spring Cloud Alibaba Nacos集成(已实现并有博文总结)
│     └── 模块名称:spring-cloud-alibaba-seata-demo模块                //Spring Cloud Alibaba Seata集成(已实现并有博文总结)
│     └── 模块名称:spring-cloud-alibaba-sentinel-demo模块             //Spring Cloud Alibaba Sentinel集成(已实现并有博文总结)
│     └── 模块名称:spring-cloud-gateway-demo模块                      //Spring Cloud Gateway集成(已实现并有博文总结)
│     └── 模块名称:spring-cloud-hystrix-demo模块                      //Spring Cloud Hystrix集成(已实现并有博文总结)
│     └── 模块名称:spring-cloud-open-feign-demo模块                   //Spring Cloud Open Feign集成(已实现并有博文总结)
│     └── 模块名称:spring-cloud-ribbon-demo模块                       //Spring Cloud Ribbon集成(已实现并有博文总结)
│     └── 模块名称:spring-cloud-security-oauth2-demo模块              //Spring Cloud Security Oauth2集成(已实现并有博文总结)
│     └── 模块名称:spring-cloud-security-oauth2-sso-client-demo模块   //Spring Cloud Security Oauth2集成(已实现并有博文总结)
│     └── 模块名称:spring-cloud-skywalking-demo模块                   //Spring Cloud Skywalking集成(已实现并有博文总结)
│     └── 模块名称:spring-cloud-stream-demo模块                       //Spring Cloud Stream集成(已实现并有博文总结)
│     └── 模块名称:swagger-demo模块                                   //springfox-swagger2集成(已实现并有博文总结)
│     └── 模块名称:xxl-job模块                                        //xxl-job集成(已实现并有博文总结)
│     └── 模块名称:apache-spark-demo模块                              //Apache Spark集成
│     └── 模块名称:etl-hdfs-hive-hbase-demo模块                       //ETL、HDFS、Hive、Hbase集成
│     └── 模块名称:ddd-mode-demo模块                                  //DDD领域设计
│     └── 模块名称:netty-demo模块                                     //Netty集成
│     └── 模块名称:vue-demo模块                                       //前端vue集成
├── document                                                        // 文档
│     └── JavaKnowledgeDocument                                     //java知识点
│           └── java基础知识点.md                     
│           └── mq知识点.md
│           └── mysql知识点.md
│           └── redis知识点.md
│           └── springcould知识点.md
│           └── spring知识点.md
│     └── FounderDocument                                           //创始人
│           └── 创始人.md


系列文章:快速集成各种微服务相关的技术,帮助大家可以快速集成到自己的项目中,节约开发时间。

提示:系列文章还未全部完成,后续的文章,会慢慢补充进去的。


文章目录

Seata Server部署:DB存储模式+Nacos(注册&配置中心)部署

步骤一:下载安装包

步骤二:建表(仅db模式)

步骤三:修改store.mode

步骤四:修改数据库连接

步骤五:配置Nacos

步骤六:启动Seata Server

Seata Client接入:(普通下单业务场景)

创建spring-cloud-alibaba-seata-demo项目

修改pom.xml

创建order-service微服务

修改pom.xml

创建bootstrap.yml

创建application.yml

修改OrderServiceApplication

创建FeignConfig

创建OrderController

创建AccountFeignService

创建FallbackAccountFeignServiceFactory

创建StorageFeignService

创建OrderService

创建OrderServiceImpl

创建OrderVo

创建db.sql

创建storage-service微服务

修改pom.xml

创建bootstrap.yml

创建application.yml

修改StorageServiceApplication

创建StorageController

创建StorageService

创建StorageServiceImpl

创建db.sql

创建account-service微服务

修改pom.xml

创建bootstrap.yml

创建application.yml

修改AccountServiceApplication

创建AccountController

创建AccountService

创建AccountServiceImpl

创建db.sql

创建mysql-common模块

创建MybatisConfig

创建Account

创建Order

创建OrderStatus

创建Storage

创建AccountMapper

创建OrderMapper

创建StorageMapper

创建ResultVo

开始验证seata是否正常工作

正常减库存

不正常减库存


官网:https://seata.io/zh-cn/index.html

源码: https://github.com/seata/seata

官方Demo: https://github.com/seata/seata-samples


Seata Server部署:DB存储模式+Nacos(注册&配置中心)部署


步骤一:下载安装包


https://github.com/seata/seata/releases


如下(示例):


e6e3238f2f3a4e58aa3a300f80b7fe79.png


步骤二:建表(仅db模式)


全局事务会话信息由3块内容构成,全局事务–>分支事务–>全局锁,对应表global_table、branch_table、lock_table


创建数据库seata,执行sql脚本,文件在script/server/db/mysql.sql(seata源码)中


如下(示例):


223253762e374e68a921e1a957ca3408.png


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_and_branch_id` (`xid` , `branch_id`)
) 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);


步骤三:修改store.mode


启动包: seata–>conf–>application.yml,修改store.mode=“db”。


步骤四:修改数据库连接


启动包: seata–>conf–>application.yml,修改store.db相关属性。


如下(示例):


f81bb494410f47219225300fe2e527e2.png


步骤五:配置Nacos


将Seata Server注册到Nacos,修改conf目录下的application.yml


如下(示例):


e56d02fbee2544fbb999e89bb8f350c6.png


application.yml文件如下(示例):

server:
  port: 7091
spring:
  application:
    name: seata-server
logging:
  config: classpath:logback-spring.xml
  file:
    path: ${user.home}/logs/seata
  extend:
    logstash-appender:
      destination: ip:4560
    kafka-appender:
      bootstrap-servers: ip:9092
      topic: logback_to_logstash
console:
  user:
    username: seata
    password: seata
seata:
  config:
    # support: nacos, consul, apollo, zk, etcd3
    type: nacos
    nacos:
      server-addr: ip:8848
      namespace: 50f661a7-0180-4277-bcee-5cfb55c213cd
      group: SEATA_GROUP
      username: nacos
      password: nacos
      ##if use MSE Nacos with auth, mutex with username/password attribute
      #access-key: ""
      #secret-key: ""
      data-id: seataServer.properties    
  registry:
    # support: nacos, eureka, redis, zk, consul, etcd3, sofa
    type: nacos
    nacos:
      application: seata-server
      server-addr: ip:8848
      group: SEATA_GROUP
      namespace: 50f661a7-0180-4277-bcee-5cfb55c213cd
      cluster: default
      username: nacos
      password: nacos
      ##if use MSE Nacos with auth, mutex with username/password attribute
      #access-key: ""
      #secret-key: ""        
  store:
    # support: file 、 db 、 redis
    mode: db
    db:
      datasource: druid
      db-type: mysql
      driver-class-name: com.mysql.jdbc.Driver
      url: jdbc:mysql://ip:3306/seata?rewriteBatchedStatements=true
      user: root
      password: root密码
      min-conn: 5
      max-conn: 100
      global-table: global_table
      branch-table: branch_table
      lock-table: lock_table
      distributed-lock-table: distributed_lock
      query-limit: 100
      max-wait: 5000
#  server:
#    service-port: 8091 #If not configured, the default is '${server.port} + 1000'
  security:
    secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
    tokenValidityInMilliseconds: 1800000
    ignore:
      urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-fe/public/**,/api/v1/auth/login


使用nacos时要注意group要和seata server中的group一致,默认group是"DEFAULT_GROUP",示例中修改成了SEATA_GROUP


seataServer.properties文件的配置在/seata/script/config-center/config.txt直接复制粘贴,然后修改配置信息


文件目录如下(示例):


b47806707b3449b3b22d8a424d0285ad.png


config.txt如下(示例):


a87285cf03fd4690881f9f521abdeb5a.png


config.txt配置如下(示例):

#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.
store.mode=db
store.lock.mode=file
store.session.mode=file
#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.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.rollbackRetryTimeoutUnlockEnable=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


nacos创建文件,粘贴配置


如下(示例):


45969b416aa6451cb517895c55963afe.png


如下(示例):


a2f7299a6031465596ee373ecd66be6a.png


需要注意的事项


配置事务分组, 要与客户端配置的事务分组一致


(客户端properties配置:spring.cloud.alibaba.seata.tx‐service‐group=default_tx_group)


如下(示例):


0f33087b221f4e65b99ddb2657ff75cb.png


步骤六:启动Seata Server


一般而言,需要修改默认端口减少黑客通过默认端口入侵服务器的可能,这里我端口修改成了9091


修改完配置之后,进行压缩,上传到服务器上,给予文件权限,进行解压,去到seata/bin目录下,启动seata-server.sh


最后使用./seata-server.sh -h 106.14.132.94 -p 8091 &启动


如下(示例):


ba18020e607948b28d473f89f52d8c16.png


由于新版本支持了skywalking而我这里没有配置它,所以有这个提示,这个不影响,我们可以查看日志:cat /opt/seata/logs/start.out发现已经启动起来了


如下(示例):


35caca45102a425c8e5310b323a432d1.png


检查安全组,防火墙的端口是否正常,然后通过部署的ip:seata配置的端口直接进行服务,application.yml中登录用户默认配置的是seata


如下(示例):


11810455f55f4083a22350a68f6f8cd4.png


浏览器直接服务登录


如下(示例):


fc9019bae67a4ce9b12174800d41b3ed.png


可切换成中文,如下(示例):


72e474c947134b69badbeea002b4a4f3.png


Seata Client接入:(普通下单业务场景)


用户下单,整个业务逻辑由三个微服务构成:


  • 库存服务:对给定的商品扣除库存数量。
  • 订单服务:根据采购需求创建订单。
  • 帐户服务:从用户帐户中扣除余额。

  • 如下(示例):


4f0c4b5809194e5f9d7a9e71cdb98c3f.png


这里开多个微服务


创建spring-cloud-alibaba-seata-demo项目


项目代码:https://gitee.com/java_wxid/java_wxid/tree/master/demo/spring-cloud-alibaba-seata-demo


项目结构如下(示例):


070545b107bb49878919094600c4eb0a.png


删除src文件夹


修改pom.xml


代码如下(示例):

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.12.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>spring-cloud-alibaba-seata-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-cloud-alibaba-seata-demo</name>
    <description>Demo project for Spring Boot</description>
    <packaging>pom</packaging>
    <modules>
        <module>mysql-common</module>
        <module>account-service</module>
        <module>order-service</module>
        <module>storage-service</module>
    </modules>
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR12</spring-cloud.version>
        <spring-cloud-alibaba.version>2.2.8.RELEASE</spring-cloud-alibaba.version>
        <seata.version>1.5.1</seata.version>
        <mysql-jdbc.version>5.1.48</mysql-jdbc.version>
        <mybatis.version>2.1.1</mybatis.version>
        <druid.version>1.2.6</druid.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring-cloud-alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <scope>runtime</scope>
                <version>${mysql-jdbc.version}</version>
            </dependency>
            <dependency>
                <groupId>io.seata</groupId>
                <artifactId>seata-all</artifactId>
                <version>${seata.version}</version>
            </dependency>
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>${mybatis.version}</version>
            </dependency>
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid</artifactId>
                <version>${druid.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>


创建order-service微服务


236173efd815417b942641ab239c5df1.png


修改pom.xml


代码如下(示例):

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.example</groupId>
        <artifactId>spring-cloud-alibaba-seata-demo</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <groupId>com.example</groupId>
    <artifactId>order-service</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>order-service</name>
    <description>Demo project for Seata</description>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- seata-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        </dependency>
        <!--nacos 注册中心-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.nacos</groupId>
            <artifactId>nacos-common</artifactId>
            <version>2.1.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!--加入sentinel-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
        <!--加入actuator-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>mysql-common</artifactId>
            <version>0.0.1-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>


创建bootstrap.yml


代码如下(示例):

#bootstrap.yml优先级比application.yml优先级高
spring:
  #prefix−{spring.profile.active}.${file-extension}
  #nacos会根据当前环境去拼接配置名称查找相应配置文件,
  #示例:{spring.application.name}-{spring.profiles.active}-{spring.cloud.nacos.config.file-extension}
  #获取到值:nacos-autoconfig-service-dev.yml
  profiles:
    #开发环境dev,测试环境test,生产环境prod
    active: dev
  application:
    #配置应用的名称,用于获取配置
    name: order-service
  cloud:
    nacos:
      discovery:
        # 服务注册地址
        server-addr: ip:8848
      config:
        #nacos配置中心地址
        server-addr: ip:8848
        #配置中心的命名空间id
        namespace: 9e50b6d9-6c3d-4e7a-b701-10f085e4b98d
        #配置分组,默认没有也可以
        group: DEFAULT_GROUP
        #配置文件后缀,用于拼接配置配置文件名称,目前只支持yaml和properties
        file-extension: yaml
        #配置自动刷新
        refresh-enabled: true
        #配置文件的前缀,默认是application.name的值,如果配了prefix,就取prefix的值
        #prefix: nacos-autoconfig-service-${spring.profile.active}
        # 配置编码
        encode: UTF-8
        username: nacos
        password: nacos


创建application.yml


代码如下(示例):

server:
  port: 8020
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      driver-class-name: com.mysql.jdbc.Driver
      url: jdbc:mysql://139.224.137.74:3306/seata_at_order?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
      username: root
      password: ca0a997ee4770063
      initial-size: 10
      max-active: 100
      min-idle: 10
      max-wait: 60000
      pool-prepared-statements: true
      max-pool-prepared-statement-per-connection-size: 20
      time-between-eviction-runs-millis: 60000
      min-evictable-idle-time-millis: 300000
      test-while-idle: true
      test-on-borrow: false
      test-on-return: false
      stat-view-servlet:
        enabled: true
        url-pattern: /druid/*
      filter:
        stat:
          log-slow-sql: true
          slow-sql-millis: 1000
          merge-sql: false
        wall:
          config:
            multi-statement-allow: true
seata:
  application-id: ${spring.application.name}
  # seata 服务分组,要与服务端配置service.vgroup_mapping的后缀对应
  tx-service-group: default_tx_group
  registry:
    # 指定nacos作为注册中心
    type: nacos
    nacos:
      application: seata-server
      server-addr: 106.14.132.94:8848
      namespace: 50f661a7-0180-4277-bcee-5cfb55c213cd
      group: SEATA_GROUP
  config:
    # 指定nacos作为配置中心
    type: nacos
    nacos:
      server-addr: 106.14.132.94:8848
      namespace: 50f661a7-0180-4277-bcee-5cfb55c213cd
      group: SEATA_GROUP
      data-id: seataServer.properties
#暴露actuator端点
management:
  endpoints:
    web:
      exposure:
        include: '*'
feign:
  sentinel:
    enabled: true


修改OrderServiceApplication


代码如下(示例):


package com.example.order;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication(scanBasePackages = "com.example")
@EnableFeignClients
public class OrderServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}


创建FeignConfig


代码如下(示例):

package com.example.order.config;
import feign.Logger;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
 * @Author: liaozhiwei
 * @Description: TODO
 * @Date: Created in 19:22 2022/8/30
 */
@Configuration
public class FeignConfig {
    @Bean
    public Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
}


创建OrderController


代码如下(示例):

package com.example.order.controller;
import com.example.datasource.entity.Order;
import com.example.datasource.vo.ResultVo;
import com.example.order.service.OrderService;
import com.example.order.vo.OrderVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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;
/**
 * @Author: liaozhiwei
 * @Description: TODO
 * @Date: Created in 19:22 2022/8/30
 */
@RestController
@RequestMapping("/order")
@Slf4j
public class OrderController {
    @Autowired
    private OrderService orderService;
    @PostMapping("/createOrder")
    public ResultVo createOrder(@RequestBody OrderVo orderVo) throws Exception {
        log.info("收到下单请求,用户:{}, 商品编号:{}", orderVo.getUserId(), orderVo.getCommodityCode());
        Order order = orderService.saveOrder(orderVo);
        return ResultVo.ok().put("order",order);
    }
}


创建AccountFeignService


代码如下(示例):

package com.example.order.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Repository;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
 * @Author: liaozhiwei
 * @Description: TODO
 * @Date: Created in 19:22 2022/8/30
 */
@FeignClient(name = "account-service",path = "/account")
@Repository
public interface AccountFeignService {
    @RequestMapping("/debit")
    Boolean debit(@RequestParam("userId") String userId,@RequestParam("money") int money);
}


创建FallbackAccountFeignServiceFactory


代码如下(示例):

package com.example.order.feign;
import io.seata.core.context.RootContext;
import io.seata.core.exception.TransactionException;
import io.seata.tm.api.GlobalTransactionContext;
import org.apache.commons.lang.StringUtils;
import feign.hystrix.FallbackFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
 * @Author: liaozhiwei
 * @Description: TODO
 * @Date: Created in 19:22 2022/8/30
 */
@Component
@Slf4j
public class FallbackAccountFeignServiceFactory implements FallbackFactory<AccountFeignService> {
    @Override
    public AccountFeignService create(Throwable throwable) {
        return new AccountFeignService() {
            @Override
            public Boolean debit(String userId, int money) {
                log.info("账户服务异常降级了");
                // 解决 feign整合sentinel降级导致Seata失效的处理  此方案不可取
                /*if(!StringUtils.isEmpty(RootContext.getXID())){
                    //通过xid获取GlobalTransaction调用rollback回滚
                    //可以让库存服务回滚  能解决问题吗?  绝对不能用
                    try {
                        GlobalTransactionContext.reload(RootContext.getXID()).rollback();
                    } catch (TransactionException e) {
                        e.printStackTrace();
                    }
                }*/
                return false;
            }
        };
    }
}


创建StorageFeignService


代码如下(示例):

package com.example.order.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Repository;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
 * @Author: liaozhiwei
 * @Description: TODO
 * @Date: Created in 19:22 2022/8/30
 */
@FeignClient(name="storage-service",path="/storage")
@Repository
public interface StorageFeignService {
    @RequestMapping(path = "/deduct")
    Boolean deduct(@RequestParam("commodityCode") String commodityCode,@RequestParam("count") Integer count);
}


创建OrderService


代码如下(示例):

package com.example.order.service;
import com.example.datasource.entity.Order;
import com.example.order.vo.OrderVo;
import io.seata.core.exception.TransactionException;
public interface OrderService {
    /**
     * 保存订单
     */
    Order saveOrder(OrderVo orderVo) throws TransactionException;
}


创建OrderServiceImpl


代码如下(示例):

package com.example.order.service.impl;
import com.example.order.feign.AccountFeignService;
import com.example.order.feign.StorageFeignService;
import com.example.order.service.OrderService;
import com.example.order.vo.OrderVo;
import com.example.datasource.entity.Order;
import com.example.datasource.entity.OrderStatus;
import com.example.datasource.mapper.OrderMapper;
import io.seata.core.context.RootContext;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
 * @Author: liaozhiwei
 * @Description: TODO
 * @Date: Created in 19:22 2022/8/30
 */
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private AccountFeignService accountFeignService;
    @Autowired
    private StorageFeignService storageFeignService;
    @Override
    //@Transactional
    @GlobalTransactional(name="createOrder",rollbackFor=Exception.class)
    public Order saveOrder(OrderVo orderVo) {
        log.info("=============用户下单=================");
        log.info("当前 XID: {}", RootContext.getXID());
        // 保存订单
        Order order = new Order();
        order.setUserId(orderVo.getUserId());
        order.setCommodityCode(orderVo.getCommodityCode());
        order.setCount(orderVo.getCount());
        order.setMoney(orderVo.getMoney());
        order.setStatus(OrderStatus.INIT.getValue());
        Integer saveOrderRecord = orderMapper.insert(order);
        log.info("保存订单{}", saveOrderRecord > 0 ? "成功" : "失败");
        //扣减库存
        storageFeignService.deduct(orderVo.getCommodityCode(), orderVo.getCount());
        //扣减余额
        Boolean debit= accountFeignService.debit(orderVo.getUserId(), orderVo.getMoney());
        if(!debit){
            // 解决 feign整合sentinel降级导致Seata失效的处理
            throw new RuntimeException("账户服务异常降级了");
        }
        //更新订单
        Integer updateOrderRecord = orderMapper.updateOrderStatus(order.getId(),OrderStatus.SUCCESS.getValue());
        log.info("更新订单id:{} {}", order.getId(), updateOrderRecord > 0 ? "成功" : "失败");
        return order;
    }
}


创建OrderVo


代码如下(示例):

package com.example.order.vo;
import lombok.Data;
/**
 * @Author: liaozhiwei
 * @Description: TODO
 * @Date: Created in 19:22 2022/8/30
 */
@Data
public class OrderVo {
    private String userId;
    /**商品编号**/
    private String commodityCode;
    private Integer count;
    private Integer money;
}


创建db.sql


代码如下(示例):

CREATE TABLE `order_tbl` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` varchar(255) DEFAULT NULL,
  `commodity_code` varchar(255) DEFAULT NULL,
  `count` int(11) DEFAULT '0',
  `money` int(11) DEFAULT '0',
  `status` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
-- for AT mode you must to init this sql for you business database. the seata server not need it.
CREATE TABLE IF NOT EXISTS `undo_log`
(
    `branch_id`     BIGINT       NOT NULL COMMENT 'branch transaction id',
    `xid`           VARCHAR(128) NOT NULL COMMENT 'global transaction id',
    `context`       VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
    `rollback_info` LONGBLOB     NOT NULL COMMENT 'rollback info',
    `log_status`    INT(11)      NOT NULL COMMENT '0:normal status,1:defense status',
    `log_created`   DATETIME(6)  NOT NULL COMMENT 'create datetime',
    `log_modified`  DATETIME(6)  NOT NULL COMMENT 'modify datetime',
    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
    ) ENGINE = InnoDB
    AUTO_INCREMENT = 1
    DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';


创建storage-service微服务


项目结构如下(示例):


7d1f2792967141be90f86445b2aff25d.png


修改pom.xml


代码如下(示例):

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.example</groupId>
        <artifactId>spring-cloud-alibaba-seata-demo</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <groupId>com.example</groupId>
    <artifactId>storage-service</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>storage-service</name>
    <description>Demo project for Seata</description>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- seata-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        </dependency>
        <!--nacos 注册中心-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.nacos</groupId>
            <artifactId>nacos-common</artifactId>
            <version>2.1.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>mysql-common</artifactId>
            <version>0.0.1-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>


创建bootstrap.yml


代码如下(示例):

#bootstrap.yml优先级比application.yml优先级高
spring:
  #prefix−{spring.profile.active}.${file-extension}
  #nacos会根据当前环境去拼接配置名称查找相应配置文件,
  #示例:{spring.application.name}-{spring.profiles.active}-{spring.cloud.nacos.config.file-extension}
  #获取到值:nacos-autoconfig-service-dev.yml
  profiles:
    #开发环境dev,测试环境test,生产环境prod
    active: dev
  application:
    #配置应用的名称,用于获取配置
    name: storage-service
  cloud:
    nacos:
      discovery:
        # 服务注册地址
        server-addr: 106.14.132.94:8848
      config:
        #nacos配置中心地址
        server-addr: 106.14.132.94:8848
        #配置中心的命名空间id
        namespace: 9e50b6d9-6c3d-4e7a-b701-10f085e4b98d
        #配置分组,默认没有也可以
        group: DEFAULT_GROUP
        #配置文件后缀,用于拼接配置配置文件名称,目前只支持yaml和properties
        file-extension: yaml
        #配置自动刷新
        refresh-enabled: true
        #配置文件的前缀,默认是application.name的值,如果配了prefix,就取prefix的值
        #prefix: nacos-autoconfig-service-${spring.profile.active}
        # 配置编码
        encode: UTF-8
        username: nacos
        password: nacos


创建application.yml


代码如下(示例):

server:
  port: 8040
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      driver-class-name: com.mysql.jdbc.Driver
      url: jdbc:mysql://139.224.137.74:3306/seata_at_storage?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
      username: root
      password: ca0a997ee4770063
      initial-size: 10
      max-active: 100
      min-idle: 10
      max-wait: 60000
      pool-prepared-statements: true
      max-pool-prepared-statement-per-connection-size: 20
      time-between-eviction-runs-millis: 60000
      min-evictable-idle-time-millis: 300000
      test-while-idle: true
      test-on-borrow: false
      test-on-return: false
      stat-view-servlet:
        enabled: true
        url-pattern: /druid/*
      filter:
        stat:
          log-slow-sql: true
          slow-sql-millis: 1000
          merge-sql: false
        wall:
          config:
            multi-statement-allow: true
seata:
  application-id: ${spring.application.name}
  # seata 服务分组,要与服务端配置service.vgroup_mapping的后缀对应
  tx-service-group: default_tx_group
  registry:
    # 指定nacos作为注册中心
    type: nacos
    nacos:
      application: seata-server
      server-addr: 106.14.132.94:8848
      namespace: 50f661a7-0180-4277-bcee-5cfb55c213cd
      group: SEATA_GROUP
  config:
    # 指定nacos作为配置中心
    type: nacos
    nacos:
      server-addr: 106.14.132.94:8848
      namespace: 50f661a7-0180-4277-bcee-5cfb55c213cd
      group: SEATA_GROUP
      data-id: seataServer.properties


修改StorageServiceApplication


代码如下(示例):

package com.example.storage;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication(scanBasePackages = "com.example")
public class StorageServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(StorageServiceApplication.class, args);
    }
}


创建StorageController


代码如下(示例):

package com.example.storage.controller;
import com.example.storage.service.StorageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * @Author: liaozhiwei
 * @Description: TODO
 * @Date: Created in 19:22 2022/8/30
 */
@RestController
@RequestMapping("/storage")
public class StorageController {
    @Autowired
    private StorageService storageService;
    @RequestMapping(path = "/deduct")
    public Boolean deduct(String commodityCode, Integer count) {
        // 扣减库存
        storageService.deduct(commodityCode, count);
        return true;
    }
}


创建StorageService


代码如下(示例):

package com.example.storage.service;
public interface StorageService {
    /**
     * 扣减库存
     * @param commodityCode 商品编号
     * @param count 扣除数量
     */
    void deduct(String commodityCode, int count);
}


创建StorageServiceImpl


代码如下(示例):

package com.example.storage.service.impl;
import com.example.datasource.entity.Storage;
import com.example.datasource.mapper.StorageMapper;
import com.example.storage.service.StorageService;
import io.seata.core.context.RootContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
 * @Author: liaozhiwei
 * @Description: TODO
 * @Date: Created in 19:22 2022/8/30
 */
@Service
@Slf4j
public class StorageServiceImpl implements StorageService {
    @Autowired
    private StorageMapper storageMapper;
    @Transactional
    @Override
    public void deduct(String commodityCode, int count){
        log.info("=============扣减库存=================");
        log.info("当前 XID: {}", RootContext.getXID());
        // 检查库存
        checkStock(commodityCode,count);
        log.info("开始扣减 {} 库存", commodityCode);
        Integer record = storageMapper.reduceStorage(commodityCode,count);
        log.info("扣减 {} 库存结果:{}", commodityCode, record > 0 ? "操作成功" : "扣减库存失败");
    }
    private void checkStock(String commodityCode, int count){
        log.info("检查 {} 库存", commodityCode);
        Storage storage = storageMapper.findByCommodityCode(commodityCode);
        if (storage.getCount() < count) {
            log.warn("{} 库存不足,当前库存:{}", commodityCode, count);
            throw new RuntimeException("库存不足");
        }
    }
}


创建db.sql


代码如下(示例):

CREATE TABLE `storage_tbl` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `commodity_code` varchar(255) DEFAULT NULL,
  `count` int(11) DEFAULT '0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `commodity_code` (`commodity_code`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
insert into `storage_tbl` (`id`, `commodity_code`, `count`) values('1','1000','1000');
-- 微服务对应数据库中添加undo_log表(仅AT模式)
CREATE TABLE IF NOT EXISTS `undo_log`
(
    `branch_id`     BIGINT       NOT NULL COMMENT 'branch transaction id',
    `xid`           VARCHAR(128) NOT NULL COMMENT 'global transaction id',
    `context`       VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
    `rollback_info` LONGBLOB     NOT NULL COMMENT 'rollback info',
    `log_status`    INT(11)      NOT NULL COMMENT '0:normal status,1:defense status',
    `log_created`   DATETIME(6)  NOT NULL COMMENT 'create datetime',
    `log_modified`  DATETIME(6)  NOT NULL COMMENT 'modify datetime',
    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
    ) ENGINE = InnoDB
    AUTO_INCREMENT = 1
    DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';


创建account-service微服务


项目结构如下(示例):


fc7b65d6ba4a4dd3a3b57e22d49f9f79.png


修改pom.xml


代码如下(示例):

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.example</groupId>
        <artifactId>spring-cloud-alibaba-seata-demo</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <groupId>com.example</groupId>
    <artifactId>account-service</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>account-service</name>
    <description>Demo project for Seata</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--nacos 注册中心-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.nacos</groupId>
            <artifactId>nacos-common</artifactId>
            <version>2.1.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!-- seata-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        </dependency>
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>mysql-common</artifactId>
            <version>0.0.1-SNAPSHOT</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>


创建bootstrap.yml


代码如下(示例):

#bootstrap.yml优先级比application.yml优先级高
spring:
  #prefix−{spring.profile.active}.${file-extension}
  #nacos会根据当前环境去拼接配置名称查找相应配置文件,
  #示例:{spring.application.name}-{spring.profiles.active}-{spring.cloud.nacos.config.file-extension}
  #获取到值:nacos-autoconfig-service-dev.yml
  profiles:
    #开发环境dev,测试环境test,生产环境prod
    active: dev
  application:
    #配置应用的名称,用于获取配置
    name: account-service
  cloud:
    nacos:
      discovery:
        # 服务注册地址
        server-addr: ip:8848
      config:
        #nacos配置中心地址
        server-addr: ip:8848
        #配置中心的命名空间id
        namespace: 9e50b6d9-6c3d-4e7a-b701-10f085e4b98d
        #配置分组,默认没有也可以
        group: DEFAULT_GROUP
        #配置文件后缀,用于拼接配置配置文件名称,目前只支持yaml和properties
        file-extension: yaml
        #配置自动刷新
        refresh-enabled: true
        #配置文件的前缀,默认是application.name的值,如果配了prefix,就取prefix的值
        #prefix: nacos-autoconfig-service-${spring.profile.active}
        # 配置编码
        encode: UTF-8
        username: nacos
        password: nacos


创建application.yml


代码如下(示例):

server:
  port: 8050
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      driver-class-name: com.mysql.jdbc.Driver
      url: jdbc:mysql://ip:3306/seata_at_account?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
      username: root
      password: ca0a997ee4770063
      initial-size: 10
      max-active: 100
      min-idle: 10
      max-wait: 60000
      pool-prepared-statements: true
      max-pool-prepared-statement-per-connection-size: 20
      time-between-eviction-runs-millis: 60000
      min-evictable-idle-time-millis: 300000
      test-while-idle: true
      test-on-borrow: false
      test-on-return: false
      stat-view-servlet:
        enabled: true
        url-pattern: /druid/*
      filter:
        stat:
          log-slow-sql: true
          slow-sql-millis: 1000
          merge-sql: false
        wall:
          config:
            multi-statement-allow: true
seata:
  application-id: ${spring.application.name}
  # seata 服务分组,要与服务端配置service.vgroup_mapping的后缀对应
  tx-service-group: default_tx_group
  registry:
    # 指定nacos作为注册中心
    type: nacos
    nacos:
      application: seata-server
      server-addr: ip:8848
      namespace: 50f661a7-0180-4277-bcee-5cfb55c213cd
      group: SEATA_GROUP
  config:
    # 指定nacos作为配置中心
    type: nacos
    nacos:
      server-addr: ip:8848
      namespace: 50f661a7-0180-4277-bcee-5cfb55c213cd
      group: SEATA_GROUP
      data-id: seataServer.properties


修改AccountServiceApplication


代码如下(示例):

package com.example.account;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication(scanBasePackages = "com.example")
public class AccountServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(AccountServiceApplication.class, args);
    }
}


创建AccountController


代码如下(示例):

package com.example.account.controller;
import com.example.account.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * @Author: liaozhiwei
 * @Description: TODO
 * @Date: Created in 19:22 2022/8/30
 */
@RestController
@RequestMapping("/account")
public class AccountController {
    @Autowired
    private AccountService accountService;
    @RequestMapping("/debit")
    public Boolean debit(String userId, int money) throws Exception {
        // 用户账户扣款
        accountService.debit(userId, money);
        return true;
    }
}


创建AccountService


代码如下(示例):

package com.example.account.service;
public interface AccountService {
    /**
     * 用户账户扣款
     * @param userId
     * @param money 从用户账户中扣除的金额
     */
    void debit(String userId, int money) ;
}


创建AccountServiceImpl


代码如下(示例):

package com.example.account.service.impl;
import com.example.account.service.AccountService;
import com.example.datasource.entity.Account;
import com.example.datasource.mapper.AccountMapper;
import io.seata.core.context.RootContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
 * @Author: liaozhiwei
 * @Description: TODO
 * @Date: Created in 19:22 2022/8/30
 */
@Service
@Slf4j
public class AccountServiceImpl implements AccountService {
    private static final String ERROR_USER_ID = "1002";
    @Autowired
    private AccountMapper accountMapper;
    /**
     * 扣减用户金额
     * @param userId
     * @param money
     */
    @Transactional
    @Override
    public void debit(String userId, int money){
        log.info("=============用户账户扣款=================");
        log.info("当前 XID: {}", RootContext.getXID());
        checkBalance(userId, money);
        log.info("开始扣减用户 {} 余额", userId);
        Integer record = accountMapper.reduceBalance(userId,money);
//        if (ERROR_USER_ID.equals(userId)) {
//            // 模拟异常
//            throw new RuntimeException("account branch exception");
//        }
        log.info("扣减用户 {} 余额结果:{}", userId, record > 0 ? "操作成功" : "扣减余额失败");
    }
    private void checkBalance(String userId, int money){
        log.info("检查用户 {} 余额", userId);
        Account account = accountMapper.selectByUserId(userId);
        if (account.getMoney() < money) {
            log.warn("用户 {} 余额不足,当前余额:{}", userId, account.getMoney());
            throw new RuntimeException("余额不足");
        }
    }
}


创建db.sql


代码如下(示例):

CREATE TABLE `account_tbl` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` varchar(255) DEFAULT NULL,
  `money` int(11) DEFAULT '0',
  PRIMARY KEY (`id`),
  UNIQUE KEY `user_id` (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
insert into `account_tbl` (`id`, `user_id`, `money`) values('1','1','1000');
-- for AT mode you must to init this sql for you business database. the seata server not need it.
CREATE TABLE IF NOT EXISTS `undo_log`
(
    `branch_id`     BIGINT       NOT NULL COMMENT 'branch transaction id',
    `xid`           VARCHAR(128) NOT NULL COMMENT 'global transaction id',
    `context`       VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
    `rollback_info` LONGBLOB     NOT NULL COMMENT 'rollback info',
    `log_status`    INT(11)      NOT NULL COMMENT '0:normal status,1:defense status',
    `log_created`   DATETIME(6)  NOT NULL COMMENT 'create datetime',
    `log_modified`  DATETIME(6)  NOT NULL COMMENT 'modify datetime',
    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
    ) ENGINE = InnoDB
    AUTO_INCREMENT = 1
    DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';


创建mysql-common模块


项目结构如下(示例):


f3eed17aa39e476aa41609f90f1f66be.png


创建MybatisConfig


代码如下(示例):

package com.example.datasource.config;
import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import javax.sql.DataSource;
/**
 * @Author: liaozhiwei
 * @Description: TODO
 * @Date: Created in 19:22 2022/8/30
 */
@Configuration
@MapperScan("com.example.datasource.mapper")
public class MybatisConfig {
    /**
     * 从配置文件获取属性构造datasource,注意前缀,这里用的是druid,根据自己情况配置,
     * 原生datasource前缀取"spring.datasource"
     *
     * @return
     */
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.druid")
    public DataSource dataSource() {
        DruidDataSource druidDataSource = new DruidDataSource();
        return druidDataSource;
    }
    @Bean(name = "sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactoryBean(DataSource dataSource) throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        //设置数据源
        factoryBean.setDataSource(dataSource);
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        factoryBean.setMapperLocations(resolver.getResources("classpath*:mybatis/**/*-mapper.xml"));
        org.apache.ibatis.session.Configuration configuration=new org.apache.ibatis.session.Configuration();
        //使用jdbc的getGeneratedKeys获取数据库自增主键值
        configuration.setUseGeneratedKeys(true);
        //使用列别名替换列名
        configuration.setUseColumnLabel(true);
        //自动使用驼峰命名属性映射字段,如userId ---> user_id
        configuration.setMapUnderscoreToCamelCase(true);
        factoryBean.setConfiguration(configuration);
        return factoryBean.getObject();
    }
}


创建Account


代码如下(示例):

package com.example.datasource.entity;
import lombok.Data;
/**
 * @Author: liaozhiwei
 * @Description: TODO
 * @Date: Created in 19:22 2022/8/30
 */
@Data
public class Account {
    private Integer id;
    private String userId;
    private Integer money;
}


创建Order


代码如下(示例):

package com.example.datasource.entity;
import lombok.Data;
/**
 * @Author: liaozhiwei
 * @Description: TODO
 * @Date: Created in 19:22 2022/8/30
 */
@Data
public class Order {
    private Integer id;
    private String userId;
    /** 商品编号 */
    private String commodityCode;
    private Integer count;
    private Integer money;
    private Integer status;
}


创建OrderStatus


代码如下(示例):

package com.example.datasource.entity;
public enum OrderStatus {
    /**
     * INIT
     */
    INIT(0),
    /**
     * SUCCESS
     */
    SUCCESS(1),
    /**
     * FAIL
     */
    FAIL(-1);
    private final int value;
    OrderStatus(int value) {
        this.value = value;
    }
    public int getValue() {
        return value;
    }
}


创建Storage


代码如下(示例):

package com.example.datasource.entity;
import lombok.Data;
/**
 * @Author: liaozhiwei
 * @Description: TODO
 * @Date: Created in 19:22 2022/8/30
 */
@Data
public class Storage {
    private Integer id;
    private String commodityCode;
    private Integer count;
}


创建AccountMapper


代码如下(示例):

package com.example.datasource.mapper;
import com.example.datasource.entity.Account;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import org.springframework.stereotype.Repository;
/**
 * @Author: liaozhiwei
 * @Description: TODO
 * @Date: Created in 19:22 2022/8/30
 */
@Repository
public interface AccountMapper {
    /**
     * 查询账户
     * @param userId
     * @return
     */
    @Select("select id, user_id, money from account_tbl WHERE user_id = #{userId}")
    Account selectByUserId(@Param("userId") String userId);
    /**
     * 扣减余额
     * @param userId 用户id
     * @param money 要扣减的金额
     * @return
     */
    @Update("update account_tbl set money =money-#{money} where user_id = #{userId}")
    int reduceBalance(@Param("userId") String userId, @Param("money") Integer money);
}


创建OrderMapper


代码如下(示例):

package com.example.datasource.mapper;
import com.example.datasource.entity.Order;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Options;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
import org.springframework.stereotype.Repository;
/**
 * @Author: liaozhiwei
 * @Description: TODO
 * @Date: Created in 19:22 2022/8/30
 */
@Repository
public interface OrderMapper {
    /**
     * 保存订单
     * @param record
     * @return
     */
    @Insert("INSERT INTO order_tbl (user_id, commodity_code, count, status, money) VALUES (#{userId}, #{commodityCode}, #{count}, #{status}, #{money})")
    @Options(useGeneratedKeys = true, keyColumn = "id", keyProperty = "id")
    int insert(Order record);
    /**
     * 更新订单状态
     * @param id
     * @param status
     * @return
     */
    @Update("UPDATE order_tbl SET status = #{status} WHERE id = #{id}")
    int updateOrderStatus(@Param("id") Integer id, @Param("status") int status);
}


创建StorageMapper


代码如下(示例):

package com.example.datasource.mapper;
import com.example.datasource.entity.Storage;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import org.springframework.stereotype.Repository;
/**
 * @Author: liaozhiwei
 * @Description: TODO
 * @Date: Created in 19:22 2022/8/30
 */
@Repository
public interface StorageMapper {
    /**
     * 获取库存
     * @param commodityCode 商品编号
     * @return
     */
    @Select("SELECT id,commodity_code,count FROM storage_tbl WHERE commodity_code = #{commodityCode}")
    Storage findByCommodityCode(@Param("commodityCode") String commodityCode);
    /**
     * 扣减库存
     * @param commodityCode 商品编号
     * @param count  要扣减的库存
     * @return
     */
    @Update("UPDATE storage_tbl SET count = count - #{count} WHERE commodity_code = #{commodityCode}")
    int reduceStorage(@Param("commodityCode") String commodityCode,@Param("count") Integer count);
}


创建ResultVo


代码如下(示例):

package com.example.datasource.vo;
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
/**
 * @Author: liaozhiwei
 * @Description: TODO
 * @Date: Created in 19:22 2022/8/30
 */
@Data
public class ResultVo extends HashMap<String, Object> {
    public ResultVo() {
        put("code", 0);
        put("msg", "success");
    }
    public static ResultVo error(int code, String msg) {
        ResultVo r = new ResultVo();
        r.put("code", code);
        r.put("msg", msg);
        return r;
    }
    public static ResultVo ok(String msg) {
        ResultVo r = new ResultVo();
        r.put("msg", msg);
        return r;
    }
    public static ResultVo ok(Map<String, Object> map) {
        ResultVo r = new ResultVo();
        r.putAll(map);
        return r;
    }
    public static ResultVo ok() {
        return new ResultVo();
    }
    @Override
    public ResultVo put(String key, Object value) {
        super.put(key, value);
        return this;
    }
}


开始验证seata是否正常工作


  • account_tbl表里面我给了一条记录,userid为1的有1000块钱;
  • storage_tbl表里面我给了一条记录,商品编号1000的库存有1000;
  • 假设这个编号1000的商品现在的价格是20块钱


正常减库存


我userid为1的用户需要买30个,走正常逻辑是可以正常下订单,订单表里会多一条记录,同时account_tbl表的金额也会减少,storage_tbl表的库存也会减少;


使用apifox或者postman调用接口:http://localhost:8020/order/createOrder


{
    "userId": "1",
    "commodityCode": "1000",
    "count": 30,
    "money": 20
}

如下(示例):


d0eb6149c51942a1a61e05ced56d1cc0.png


account_tbl表数据变化


如下(示例):


1a9e5bd6d07c4f55a400797534fbf976.png


order_tbl表数据变化


如下(示例):


09096b6d1d234b7bb76123f0258aa4fa.png


storage_tbl表数据变化


如下(示例):


57a7468fef3a478b950fdf8e26c523ff.png


订单服务控制台打印:


如下(示例):


42847119e29b4793af9921b6e028b754.png


不正常减库存


我userid为1的用户需要买10000个,走正常逻辑是不可以正常下订单,第一个钱不够,第二个库存也不够,所以订单表里不会多一条记录,同时account_tbl表的金额不会改变,storage_tbl表的库存不会改变;


使用apifox或者postman调用接口:http://localhost:8020/order/createOrder


{
    "userId": "1",
    "commodityCode": "1000",
    "count": 10000,
    "money": 20
}

如下(示例):


d35025f43bbd4231b5634e1039375502.png


订单服务控制台打印:


如下(示例):


8885d6ca2eb54ab9bfaf87632382f27e.png


库存服务控制台打印:


如下(示例):


4615b4fe913044feb34faab13ab87144.png


三个表都没有发生变化,业务逻辑正确,seata可以正常工作。

相关实践学习
以电商场景为例搭建AI语义搜索应用
本实验旨在通过阿里云Elasticsearch结合阿里云搜索开发工作台AI模型服务,构建一个高效、精准的语义搜索系统,模拟电商场景,深入理解AI搜索技术原理并掌握其实现过程。
ElasticSearch 最新快速入门教程
本课程由千锋教育提供。全文搜索的需求非常大。而开源的解决办法Elasricsearch(Elastic)就是一个非常好的工具。目前是全文搜索引擎的首选。本系列教程由浅入深讲解了在CentOS7系统下如何搭建ElasticSearch,如何使用Kibana实现各种方式的搜索并详细分析了搜索的原理,最后讲解了在Java应用中如何集成ElasticSearch并实现搜索。 &nbsp;
相关文章
|
监控 Java 数据库
从零学 Dropwizard:手把手搭轻量 Java 微服务,告别 Spring 臃肿
Dropwizard 整合 Jetty、Jersey 等成熟组件,开箱即用,无需复杂配置。轻量高效,启动快,资源占用少,内置监控、健康检查与安全防护,搭配 Docker 部署便捷,是构建生产级 Java 微服务的极简利器。
1203 117
|
12月前
|
安全 前端开发 Java
《深入理解Spring》:现代Java开发的核心框架
Spring自2003年诞生以来,已成为Java企业级开发的基石,凭借IoC、AOP、声明式编程等核心特性,极大简化了开发复杂度。本系列将深入解析Spring框架核心原理及Spring Boot、Cloud、Security等生态组件,助力开发者构建高效、可扩展的应用体系。(238字)
|
人工智能 Java API
构建基于Java的AI智能体:使用LangChain4j与Spring AI实现RAG应用
当大模型需要处理私有、实时的数据时,检索增强生成(RAG)技术成为了核心解决方案。本文深入探讨如何在Java生态中构建具备RAG能力的AI智能体。我们将介绍新兴的Spring AI项目与成熟的LangChain4j框架,详细演示如何从零开始构建一个能够查询私有知识库的智能问答系统。内容涵盖文档加载与分块、向量数据库集成、语义检索以及与大模型的最终合成,并提供完整的代码实现,为Java开发者开启构建复杂AI智能体的大门。
6175 58
|
12月前
|
消息中间件 缓存 Java
Spring框架优化:提高Java应用的性能与适应性
以上方法均旨在综合考虑Java Spring 应该程序设计原则, 数据库交互, 编码实践和系统架构布局等多角度因素, 旨在达到高效稳定运转目标同时也易于未来扩展.
938 8
|
资源调度 JavaScript 前端开发
在Vue 3项目中集成Element Plus组件库的步骤
总结起来,在集成过程当中我们关注于库本身提供功能与特性、环境搭建与依赖管理、模块化编程思想以及前端工程化等方面知识点;同时也涵盖前端性能优化(比如上文提及“按需加载”)与定制化开发(例如“自定义主题”)等高级话题.
993 16
|
安全 Java 数据库
SpringSecurity认证授权及项目集成
本文介绍了基于Spring Security的权限管理框架,涵盖认证、授权与鉴权核心概念,通过快速入门示例演示集成流程,并结合数据库实现用户认证。进一步扩展实现正常登录,JWT登录及鉴权管理器,实现灵活的安全控制,适用于前后端分离项目中的权限设计与实践。
968 4
|
前端开发 Java 开发者
Java新手指南:在Spring MVC中使用查询字符串与参数
通过结合实际的需求和业务逻辑,开发者可以灵活地利用这些机制,为用户提供更丰富而高效的Web应用体验。
302 15
|
安全 Java 微服务
Java 最新技术和框架实操:涵盖 JDK 21 新特性与 Spring Security 6.x 安全框架搭建
本文系统整理了Java最新技术与主流框架实操内容,涵盖Java 17+新特性(如模式匹配、文本块、记录类)、Spring Boot 3微服务开发、响应式编程(WebFlux)、容器化部署(Docker+K8s)、测试与CI/CD实践,附完整代码示例和学习资源推荐,助你构建现代Java全栈开发能力。
1153 1
|
Cloud Native Java API
Java Spring框架技术栈选和最新版本及发展史详解(截至2025年8月)-优雅草卓伊凡
Java Spring框架技术栈选和最新版本及发展史详解(截至2025年8月)-优雅草卓伊凡
1857 0
|
Java 数据库连接 数据库