Spring Cloud Alibaba系列(四):Seata+Nacos+MyBatis Plus+Open Feign实现分布式事务(实践篇)

1,280 阅读9分钟

在上篇文章中我们已经介绍了分布式事务的相关概念,以及几种理论模式。这篇文章将从实践出发,基于Seata的AT模式,借助于电商下单的业务场景,实现一个例子。

本文代码示例:gitee.com/zhaowenyi/s…

1. 准备工作

本文将以电商系统中购买商品,扣减商品库存,扣划账户余额为应用场景,基于Seata的AT模式实现一个简单的案例。

1.1 框架准备

  • JDK 1.8
  • Nacos Server 1.4.2
  • Seata Server 1.4.2

1.2 数据库准备


-- -------------------------------- 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_gmt_modified_status` (`gmt_modified`, `status`),
    KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

-- 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 = utf8;

-- 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),
    `gmt_create`     DATETIME,
    `gmt_modified`   DATETIME,
    PRIMARY KEY (`row_key`),
    KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;
  
-- seata 框架必备undo_log表
CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  `ext` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;


-- 库存表
DROP TABLE IF EXISTS `storage_tbl`;
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`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO storage_tbl (commodity_code, count) VALUES ("product_001", 10);
INSERT INTO storage_tbl (commodity_code, count) VALUES ("product_002", 20);
INSERT INTO storage_tbl (commodity_code, count) VALUES ("product_003", 30);

-- 订单表
DROP TABLE IF EXISTS `order_tbl`;
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,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- 账户余额表
DROP TABLE IF EXISTS `account_tbl`;
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`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

insert into account_tbl (user_id, money) VALUES (1, 1000);
insert into account_tbl (user_id, money) VALUES (2, 2000);
insert into account_tbl (user_id, money) VALUES (3, 3000);

1.3 脚手架准备

本文中使用Aliyun提供的脚手架

start.aliyun.com/bootstrap.h…

输入group和项目名后,下一步选择组件依赖

image.png

组建依赖:

  • Spring Cloud Alibaba Seata
  • Nacos Service Discovery
  • Arthas
  • Spring boot devtools
  • Lombok
  • Spring Configuration Processor
  • MyBatis Plus Framework
  • Validation
  • Junit
  • Spring web

image.png

可以初步浏览代码

image.png

点击获取代码即可将代码获取下来

image.png

1.4 Seata安装

官方安装教程:seata.io/zh-cn/docs/…

image.png

  • 拉到最下面,选择压缩包下载并解压 image.png

  • 进入到bin目录,执行以下命令即可启动seata,默认端口是8091

$ sh ./bin/seata-server.sh
  • 启动成功后如下

image.png

1.5 Nacos 启动

Nacos 安装就不在此赘述了,进入nacos的bin目录执行以下命令

$ sh startup.sh -m standalone

启动成功后如下

image.png

控制台地址是:http://localhost:8848/nacos/

image.png

1.6 Seata 整合 Nacos

Seata如果使用Nacos作为配置中心,则需要进行相关的配置修改。其中registry.conf配置的是注册中心和配置中心的方式,默认是file,本文中将使用nacos作为配置中心和注册中心。

  • 进入 Seata的conf目录,修改registry.conf 文件内容

image.png

  • 将 type 由file修改成nacos,并配置nacos 的用户名和密码。注意registry和config都需要修改成nacos

image.png

image.png

  • 修改file.conf文件内容

将type改成db, 配置本机数据的用户名密码

image.png

image.png

  • 下载nacos config

github.com/seata/seata… 中下载nacos-config.sh,并放到conf目录下

image.png

github.com/seata/seata… 中下载config.txt,并放到根目录下,注意不是conf目录

image.png

  • 修改config.txt文件

image.png

image.png

在conf目录下执行sh nacos-config.sh 127.0.0.1,其中127.0.0.1是nacos地址

image.png

然后查看nacos配置中心,发现配置都发布到nacos上了

image.png

  • 最终的registry.conf文件
registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "nacos"

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

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

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

  • 最终的file.conf文件

service {
  #transaction service group mapping
  vgroupMapping.seata-demo-account="default"
  vgroupMapping.seata-demo-order="default"
  vgroupMapping.seata-demo-storage="default"
  default.grouplist="127.0.0.1:8091"
  disableGlobalTransaction=false
}

## transaction log store, only used in seata-server
store {
  ## store mode: file、db、redis
  mode = "db"
  ## rsa decryption public key
  publicKey = ""
  ## file store property
  file {
    ## store location dir
    dir = "sessionStore"
    # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
    maxBranchSessionSize = 16384
    # globe session size , if exceeded throws exceptions
    maxGlobalSessionSize = 512
    # file buffer size , if exceeded allocate new buffer
    fileWriteBufferCacheSize = 16384
    # when recover batch read size
    sessionReloadReadSize = 100
    # async, sync
    flushDiskMode = async
  }

  ## database store property
  db {
    ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
    datasource = "druid"
    ## mysql/oracle/postgresql/h2/oceanbase etc.
    dbType = "mysql"
    driverClassName = "com.mysql.jdbc.Driver"
    ## if using mysql to store the data, recommend add rewriteBatchedStatements=true in jdbc connection param
    url = "jdbc:mysql://127.0.0.1:3306/spring_cloud_alibaba?rewriteBatchedStatements=true"
    user = "root"
    password = "123456"
    minConn = 5
    maxConn = 100
    globalTable = "global_table"
    branchTable = "branch_table"
    lockTable = "lock_table"
    queryLimit = 100
    maxWait = 5000
  }

  ## redis store property
  redis {
    ## redis mode: single、sentinel
    mode = "single"
    ## single mode property
    single {
      host = "127.0.0.1"
      port = "6379"
    }
    ## sentinel mode property
    sentinel {
      masterName = ""
      ## such as "10.28.235.65:26379,10.28.235.65:26380,10.28.235.65:26381"
      sentinelHosts = ""
    }
    password = ""
    database = "0"
    minConn = 1
    maxConn = 10
    maxTotal = 100
    queryLimit = 100
  }
}

  • 最终的config.txt
transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableClientBatchSendRequest=true
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
service.vgroupMapping.seata-demo-storage=default
service.vgroupMapping.seata-demo-order=default
service.vgroupMapping.seata-demo-account=default
service.default.grouplist=127.0.0.1:8091
service.enableDegrade=false
service.disableGlobalTransaction=false
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=false
client.rm.tableMetaCheckerInterval=60000
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
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
store.mode=file
store.lock.mode=file
store.session.mode=file
store.publicKey=
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
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/spring_cloud_alibaba?useUnicode=true&rewriteBatchedStatements=true
store.db.user=root
store.db.password=123456
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
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
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
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
log.exceptionRate=100
transport.serialization=seata
transport.compressor=none
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898

  • 启动Seata

在bin目录下执行./seata-server.sh -h 127.0.0.1 -p 8091 -m db,其中8091是Seata Server端口

image.png

1.7 项目配置

  • 将上面的file.conf和registry.conf复制到每个项目的resource目录下

image.png

  • 修改application.properties,添加nacos配置和数据源配置
# 应用名称
spring.application.name=seata-demo-storage

# 应用服务 WEB 访问端口
server.port=8080

# Nacos帮助文档: https://nacos.io/zh-cn/docs/concepts.html
# Nacos认证信息
spring.cloud.nacos.discovery.username=nacos
spring.cloud.nacos.discovery.password=nacos
# Nacos 服务发现与注册配置,其中子属性 server-addr 指定 Nacos 服务器主机和端口
spring.cloud.nacos.discovery.server-addr=localhost:8848
# 注册到 nacos 的指定 namespace,默认为 public
spring.cloud.nacos.discovery.namespace=public

spring.cloud.alibaba.seata.tx-service-group=seata-demo-storage

# datasource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.url=jdbc:mysql://localhost:3306/spring_cloud_alibaba?useUnicode=true&amp&characterEncoding=UTF-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=123456
  • 启动项目,项目启动成功后,会打印如下日志。至此我们才完成了基本的准备工作,接下来就是业务逻辑开发了。
2021-08-13 17:14:56.131  INFO 81427 --- [  restartedMain] c.a.c.n.registry.NacosServiceRegistry    : nacos registry, DEFAULT_GROUP seata-demo-storage 172.18.38.14:8080 register finished
2021-08-13 17:14:56.156  INFO 81427 --- [  restartedMain] c.e.s.SeataDemoStorageApplication        : Started SeataDemoStorageApplication in 13.745 seconds (JVM running for 17.071)

2. 架构设计

用户购买商品的业务逻辑。整个业务逻辑由3个微服务提供支持:

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

image.png

3. seata-demo-account 账户服务

3.1 AccountController

/**
 * @ClassName : AccountController
 * @Description : 账户
 * @Author : zhaowenyi
 * @Date: 2021/08/13
 */
@RestController
public class AccountController {

    @Autowired
    AccountTblService accountTblService;

    @PostMapping("/api/account/debit")
    public void debit(@RequestParam("userId") String userId,
                      @RequestParam("money")  int money) {
        accountTblService.debit(userId, money);
    }
}

3.2 AccountController

/**
* 账户
*/
@Service
public class AccountTblServiceImpl extends ServiceImpl<AccountTblMapper, AccountTbl>
implements AccountTblService {

    @Override
    public void debit(String userId, int money) {
        if(StringUtils.equals(userId, "2")) {
            throw new RuntimeException("模拟异常");
        }
        var wrapper = new LambdaUpdateWrapper<AccountTbl>();
        wrapper.setSql("money = money - " + money)
                .eq(AccountTbl::getUserId, userId);
        this.update(wrapper);
    }
}

4. seata-demo-storage 库存服务

4.1 StorageController


/**
 * @ClassName : StorageController
 * @Description : 库存服务
 * @Author : zhaowenyi
 * @Date: 2021/08/13
 */
@RestController
public class StorageController {

    @Autowired
    StorageTblService storageTblService;

    @PostMapping(value = "/api/storage/debuct")
    public void debuct(@RequestParam(value = "commodityCode", required = true) String commodityCode,
                       @RequestParam(value = "count", required = true) Integer count) {
        storageTblService.deduct(commodityCode, count);
    }
}

4.2 StorageTblServiceImpl

/**
* 库存
*/
@Service
public class StorageTblServiceImpl extends ServiceImpl<StorageTblMapper, StorageTbl>
implements StorageTblService {

    @Override
    public void deduct(String commodityCode, int count) {
        var wrapper = new LambdaUpdateWrapper<StorageTbl>();
        wrapper.setSql("count = count - " + count)
                .eq(StorageTbl::getCommodityCode, commodityCode);
        this.update(wrapper);
    }
}

5. seata-demo-storage 库存服务

5.1 OrderController


/**
 * @ClassName : OrderController
 * @Description : 订单服务
 * @Author : zhaowenyi
 * @Date: 2021/08/13
 */
@RestController
public class OrderController {

    @Autowired
    OrderTblService orderTblService;

    @PostMapping("api/order/create")
    public void create(String userId, String commodityCode, int orderCount) {
        orderTblService.create(userId, commodityCode, orderCount);
    }
}

5.2 OrderTblServiceImpl

要实现全局事务,在创建订单的时候加上@GlobalTransactional注解即可

/**
* 订单服务
*/
@Service
public class OrderTblServiceImpl extends ServiceImpl<OrderTblMapper, OrderTbl>
implements OrderTblService {

    @Autowired
    AccountFeignClient accountFeignClient;

    @Autowired
    StorageFeignClient storageFeignClient;

    @GlobalTransactional
    @Override
    public OrderTbl create(String userId, String commodityCode, int orderCount) {
        OrderTbl order = new OrderTbl();
        order.setCommodityCode(commodityCode);
        order.setCount(orderCount);
        order.setMoney(orderCount * 100);
        order.setUserId(userId);
        this.save(order);
        
        // 扣减余额
        accountFeignClient.debit(userId, order.getMoney());
        // 扣减库存
        storageFeignClient.debuct(commodityCode, orderCount);
        return null;
    }
}

6. 测试

6.1 注释@GlobalTransactional注解

  • 初始化的数据

image.png

image.png

image.png

  • 模拟正常情况,用户1,购买商品product_001
curl --location --request POST 'localhost:8082/api/order/create?userId=1&orderCount=1&commodityCode=product_001' \
--header 'Cookie: JSESSIONID=BEA387BFD6640DE11364C8A10C11A85C'

image.png

订单表多了一条数据

image.png

账户表少了100 image.png

库存表少了1

image.png

  • 模拟异常情况,用户2,购买商品product_001

image.png

调用扣划账户余额失败


feign.FeignException$InternalServerError: [500] during [POST] to [http://seata-demo-account/api/account/debit?userId=2&money=100] [AccountFeignClient#debit(String,int)]: [{"timestamp":"2021-08-13T12:11:38.571+00:00","status":500,"error":"Internal Server Error","trace":"java.lang.RuntimeException: 模拟异常\n\tat com.elio.seatademoaccount.impl.AccountTblServiceImpl.debit(Acc... (5528 bytes)]
	at feign.FeignException.serverErrorStatus(FeignException.java:231) ~[feign-core-10.10.1.jar:na]
	at feign.FeignException.errorStatus(FeignException.java:180) ~[feign-core-10.10.1.jar:na]
	at feign.FeignException.errorStatus(FeignException.java:169) ~[feign-core-10.10.1.jar:na]
	at feign.codec.ErrorDecoder$Default.decode(ErrorDecoder.java:92) ~[feign-core-10.10.1.jar:na]
	at feign.AsyncResponseHandler.handleResponse(AsyncResponseHandler.java:96) ~[feign-core-10.10.1.jar:na]
	at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:138) ~[feign-core-10.10.1.jar:na]
	at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:89) ~[feign-core-10.10.1.jar:na]
	at feign.ReflectiveFeign$FeignInvocationHandler.invoke(ReflectiveFeign.java:100) ~[feign-core-10.10.1.jar:na]
	at com.sun.proxy.$Proxy98.debit(Unknown Source) ~[na:na]
	at com.elio.seatademoorder.impl.OrderTblServiceImpl.create(OrderTblServiceImpl.java:38) ~[classes/:na]

订单表多了一条数据 image.png

账户表此时没有扣钱!!! image.png

库存表也没有扣减库存!!! image.png

这说明创建订单服务不是原子操作,创建订单成功,扣减账户余额和库存都失败了。

6.2 加上@GlobalTransactional注解

当在创建订单时加上@GlobalTransactional注解,模拟异常情况

image.png

订单表此时没有创建新的订单 image.png

此时账户也没有扣减余额 image.png

在看下日志,在调用扣划账户余额失败,Seata对订单服务进行了回滚

2021-08-13 20:17:01.110  INFO 91060 --- [ch_RMROLE_1_1_8] i.s.c.r.p.c.RmBranchRollbackProcessor    : rm handle branch rollback process:xid=172.18.41.122:8091:7458126500631044112,branchId=7458126500631044115,branchType=AT,resourceId=jdbc:mysql://localhost:3306/spring_cloud_alibaba,applicationData=null
2021-08-13 20:17:01.112  INFO 91060 --- [ch_RMROLE_1_1_8] io.seata.rm.AbstractRMHandler            : Branch Rollbacking: 172.18.41.122:8091:7458126500631044112 7458126500631044115 jdbc:mysql://localhost:3306/spring_cloud_alibaba
2021-08-13 20:17:01.196  INFO 91060 --- [ch_RMROLE_1_1_8] i.s.r.d.undo.AbstractUndoLogManager      : xid 172.18.41.122:8091:7458126500631044112 branch 7458126500631044115, undo_log deleted with GlobalFinished
2021-08-13 20:17:01.198  INFO 91060 --- [ch_RMROLE_1_1_8] io.seata.rm.AbstractRMHandler            : Branch Rollbacked result: PhaseTwo_Rollbacked
2021-08-13 20:17:01.224  INFO 91060 --- [nio-8082-exec-2] i.seata.tm.api.DefaultGlobalTransaction  : [172.18.41.122:8091:7458126500631044112] rollback status: Rollbacked
2021-08-13 20:17:01.263 ERROR 91060 --- [nio-8082-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is feign.FeignException$InternalServerError: [500] during [POST] to [http://seata-demo-account/api/account/debit?userId=2&money=100] [AccountFeignClient#debit(String,int)]: [{"timestamp":"2021-08-13T12:17:01.050+00:00","status":500,"error":"Internal Server Error","trace":"java.lang.RuntimeException: 模拟异常\n\tat com.elio.seatademoaccount.impl.AccountTblServiceImpl.debit(Acc... (5528 bytes)]] with root cause

feign.FeignException$InternalServerError: [500] during [POST] to [http://seata-demo-account/api/account/debit?userId=2&money=100] [AccountFeignClient#debit(String,int)]: [{"timestamp":"2021-08-13T12:17:01.050+00:00","status":500,"error":"Internal Server Error","trace":"java.lang.RuntimeException: 模拟异常\n\tat com.elio.seatademoaccount.impl.AccountTblServiceImpl.debit(Acc... (5528 bytes)]
	at feign.FeignException.serverErrorStatus(FeignException.java:231) ~[feign-core-10.10.1.jar:na]
	at feign.FeignException.errorStatus(FeignException.java:180) ~[feign-core-10.10.1.jar:na]
	at feign.FeignException.errorStatus(FeignException.java:169) ~[feign-core-10.10.1.jar:na]
	at feign.codec.ErrorDecoder$Default.decode(ErrorDecoder.java:92) ~[feign-core-10.10.1.jar:na]
	at feign.AsyncResponseHandler.handleResponse(AsyncResponseHandler.java:96) ~[feign-core-10.10.1.jar:na]
	at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:138) ~[feign-core-10.10.1.jar:na]
	at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:89) ~[feign-core-10.10.1.jar:na]
	at feign.ReflectiveFeign$FeignInvocationHandler.invoke(ReflectiveFeign.java:100) ~[feign-core-10.10.1.jar:na]
	at com.sun.proxy.$Proxy99.debit(Unknown Source) ~[na:na]
	at com.elio.seatademoorder.impl.OrderTblServiceImpl.create(OrderTblServiceImpl.java:38) ~[classes/:na]

总结

本文通过电商中很简单的一个案例给大家展示了Seata的厉害,除了在一开始安装Seata时花费了很多时间,后续的分布式事务只需要加上一个@GlobalTransactional注解接口,真正的做到了无业务入侵,并且方便快捷。但是Seata是如何做到分布式事务的控制呢,对我们来说还是个黑盒。笔者将在接下来的文章中深入探讨Seata的工作原理