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

本文涉及的产品
检索分析服务 Elasticsearch 版,2核4GB开发者规格 1个月
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 主项目链接: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可以正常工作。

相关实践学习
使用阿里云Elasticsearch体验信息检索加速
通过创建登录阿里云Elasticsearch集群,使用DataWorks将MySQL数据同步至Elasticsearch,体验多条件检索效果,简单展示数据同步和信息检索加速的过程和操作。
ElasticSearch 入门精讲
ElasticSearch是一个开源的、基于Lucene的、分布式、高扩展、高实时的搜索与数据分析引擎。根据DB-Engines的排名显示,Elasticsearch是最受欢迎的企业搜索引擎,其次是Apache Solr(也是基于Lucene)。 ElasticSearch的实现原理主要分为以下几个步骤: 用户将数据提交到Elastic Search 数据库中 通过分词控制器去将对应的语句分词,将其权重和分词结果一并存入数据 当用户搜索数据时候,再根据权重将结果排名、打分 将返回结果呈现给用户 Elasticsearch可以用于搜索各种文档。它提供可扩展的搜索,具有接近实时的搜索,并支持多租户。
相关文章
|
27天前
|
存储 缓存 Java
Java中的分布式缓存与Memcached集成实战
通过在Java项目中集成Memcached,可以显著提升系统的性能和响应速度。合理的缓存策略、分布式架构设计和异常处理机制是实现高效缓存的关键。希望本文提供的实战示例和优化建议能够帮助开发者更好地应用Memcached,实现高性能的分布式缓存解决方案。
39 9
|
2月前
|
人工智能 自然语言处理 Java
FastExcel:开源的 JAVA 解析 Excel 工具,集成 AI 通过自然语言处理 Excel 文件,完全兼容 EasyExcel
FastExcel 是一款基于 Java 的高性能 Excel 处理工具,专注于优化大规模数据处理,提供简洁易用的 API 和流式操作能力,支持从 EasyExcel 无缝迁移。
234 9
FastExcel:开源的 JAVA 解析 Excel 工具,集成 AI 通过自然语言处理 Excel 文件,完全兼容 EasyExcel
|
1月前
|
人工智能 安全 Java
AI 时代:从 Spring Cloud Alibaba 到 Spring AI Alibaba
本次分享由阿里云智能集团云原生微服务技术负责人李艳林主讲,主题为“AI时代:从Spring Cloud Alibaba到Spring AI Alibaba”。内容涵盖应用架构演进、AI agent框架发展趋势及Spring AI Alibaba的重磅发布。分享介绍了AI原生架构与传统架构的融合,强调了API优先、事件驱动和AI运维的重要性。同时,详细解析了Spring AI Alibaba的三层抽象设计,包括模型支持、工作流智能体编排及生产可用性构建能力,确保安全合规、高效部署与可观测性。最后,结合实际案例展示了如何利用私域数据优化AI应用,提升业务价值。
141 4
|
1月前
|
Java API Apache
java集成stable diffusion
通过REST API和JNI两种方法,我们可以在Java应用程序中集成Stable Diffusion模型。REST API方法更加简单和易于维护,而JNI方法则提供更高的性能。根据具体应用场景和需求,选择合适的集成方法,可以充分利用Stable Diffusion的强大功能,实现高效的图像生成和处理。
68 15
|
1月前
|
人工智能 Java API
阿里云工程师跟通义灵码结伴编程, 用Spring AI Alibaba来开发 AI 答疑助手
本次分享的主题是阿里云工程师跟通义灵码结伴编程, 用Spring AI Alibaba来开发 AI 答疑助手,由阿里云两位工程师分享。
阿里云工程师跟通义灵码结伴编程, 用Spring AI Alibaba来开发 AI 答疑助手
|
2月前
|
人工智能 前端开发 Java
Spring AI Alibaba + 通义千问,开发AI应用如此简单!!!
本文介绍了如何使用Spring AI Alibaba开发一个简单的AI对话应用。通过引入`spring-ai-alibaba-starter`依赖和配置API密钥,结合Spring Boot项目,只需几行代码即可实现与AI模型的交互。具体步骤包括创建Spring Boot项目、编写Controller处理对话请求以及前端页面展示对话内容。此外,文章还介绍了如何通过添加对话记忆功能,使AI能够理解上下文并进行连贯对话。最后,总结了Spring AI为Java开发者带来的便利,简化了AI应用的开发流程。
909 0
|
3月前
|
存储 Java 关系型数据库
在Spring Boot中整合Seata框架实现分布式事务
可以在 Spring Boot 中成功整合 Seata 框架,实现分布式事务的管理和处理。在实际应用中,还需要根据具体的业务需求和技术架构进行进一步的优化和调整。同时,要注意处理各种可能出现的问题,以保障分布式事务的顺利执行。
214 53
|
1月前
|
人工智能 自然语言处理 Java
Spring Cloud Alibaba AI 入门与实践
本文将介绍 Spring Cloud Alibaba AI 的基本概念、主要特性和功能,并演示如何完成一个在线聊天和在线画图的 AI 应用。
357 7
|
2月前
|
Java Nacos Sentinel
Spring Cloud Alibaba:一站式微服务解决方案
Spring Cloud Alibaba(简称SCA) 是一个基于 Spring Cloud 构建的开源微服务框架,专为解决分布式系统中的服务治理、配置管理、服务发现、消息总线等问题而设计。
463 13
Spring Cloud Alibaba:一站式微服务解决方案
|
1月前
|
Java 关系型数据库 数据库
微服务SpringCloud分布式事务之Seata
SpringCloud+SpringCloudAlibaba的Seata实现分布式事务,步骤超详细,附带视频教程
77 1