我们在使用微服务架构的时候,难免会遇到跨服务操作数据库的情形。例如,创建订单的业务,我们需要在订单服务生成订单数据,同时要在商品服务进行库存扣减。此时的操作涉及到订单、商品两个服务的数据操作,如何保证同时成功或同时失败呢?对此,本篇将介绍使用 Seata 来解决此问题。
1. 关于 Seata
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。关于 Seata 的更多详细介绍,请参考官方文档(链接地址)。
2. 版本选择
由于我们 Spring Cloud Alibaba 选择的是 2022.0.0.0 版本,其中的 Seata 版本为 1.7.0,我们的 Seata 也选择版本为 1.7.0。
3. Seata Server 搭建
步骤一:从 链接地址 下载服务端安装包 seata-server-1.7.0.tar.gz。
步骤二:使用 tar zxvf seata-server-1.7.0.tar.gz 命令解压。
步骤三:找到“/seata/script/server/db ”目录下的 mysql.sql 文件。创建数据库 mall-seata,并执行mysql.sql 文件脚本,初始化服务端表结构。
步骤四:修改“/seata/conf”目录下的 application.yml 文件。
将配置中心、注册中心都修改为 Nacos,内容如下。
server:
port: 7091
spring:
application:
name: seata-server
logging:
config: classpath:logback-spring.xml
file:
path: ${user.home}/logs/seata
extend:
logstash-appender:
destination: 127.0.0.1:4560
kafka-appender:
bootstrap-servers: 127.0.0.1:9092
topic: logback_to_logstash
console:
user:
username: seata
password: seata
seata:
config:
# support: nacos 、 consul 、 apollo 、 zk 、 etcd3
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: dev
group: SEATA_GROUP
username:
password:
context-path:
##if use MSE Nacos with auth, mutex with username/password attribute
#access-key:
#secret-key:
data-id: seata-server.properties
registry:
# support: nacos 、 eureka 、 redis 、 zk 、 consul 、 etcd3 、 sofa
type: nacos
preferred-networks: 30.240.*
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
namespace: dev
cluster: default
username:
password:
context-path:
##if use MSE Nacos with auth, mutex with username/password attribute
#access-key:
#secret-key:
server:
service-port: 8091 #If not configured, the default is '${server.port} + 1000'
max-commit-retry-timeout: -1
max-rollback-retry-timeout: -1
rollback-retry-timeout-unlock-enable: false
enable-check-auth: true
enable-parallel-request-handle: true
retry-dead-threshold: 130000
xaer-nota-retry-timeout: 60000
enableParallelRequestHandle: true
recovery:
committing-retry-period: 1000
async-committing-retry-period: 1000
rollbacking-retry-period: 1000
timeout-retry-period: 1000
undo:
log-save-days: 7
log-delete-period: 86400000
session:
branch-async-queue-size: 5000 #branch async remove queue size
enable-branch-async-remove: false #enable to asynchronous remove branchSession
#store:
# support: file 、 db 、 redis
#mode: db
metrics:
enabled: false
registry-type: compact
exporter-list: prometheus
exporter-prometheus-port: 9898
transport:
rpc-tc-request-timeout: 15000
enable-tc-server-batch-send-response: false
shutdown:
wait: 3
thread-factory:
boss-thread-prefix: NettyBoss
worker-thread-prefix: NettyServerNIOWorker
boss-thread-size: 1
# 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,/**/*.jpeg,/**/*.ico,/api/v1/auth/login
上面的 seata-server.properties 文件,从 Nacos 中读取,用以配置分组和会话存储,内容如下。
#Transaction routing rules configuration, only for the client
service.vgroupMapping.my_test_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 storage configuration, only for the server. The file, db, and redis configuration values are optional.
store.mode=db
store.lock.mode=db
store.session.mode=db
#Used for password encryption
#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.cj.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/mall-seata?rewriteBatchedStatements=true
store.db.user=root
store.db.password=root
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
步骤五:进入到 “/seata/bin”目录,执行 “./seata-server.sh -p 8091 -h 127.0.0.1 -m db”命令即可启动服务。
步骤六:检查服务启动情况,执行“cat /soft/seata/logs/start.out ”,看到启动日志没报错,说明服务已经起来了。
打开 Nacos 服务列表,看到 seata-server 也注册上来了。
浏览器输入“ http://127.0.0.1:7091”地址,访问 Seata 控制台。
输入用户名:seata,密码:seata,登录结果如下。
4. Seata 整合
在 Nacos 上创建 Seata 客户端配置文件,名称为:seata-client.yaml,内容如下。
seata:
enabled: true
# 指定事务分组至集群映射关系,集群名default需要与seata-server注册到Nacos的cluster保持一致
service:
vgroupMapping:
my_test_tx_group: default
default:
grouplist: ${SEATA_SERVER_ADDR}
enableDegrade: false
disableGlobalTransaction: false
# 事务分组配置
tx-service-group: my_test_tx_group
其中 ${SEATA_SERVER_ADDR} 为 seata-server 的地址,例如:127.0.0.1:8091。
账户模块、商品模块、订单模块的 domain 工程添加如下 Seata 依赖。
<!-- seata -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
账户模块、商品模块、订单模块 start 工程的 bootstrap.yml 文件添加 seata-client.yaml 引用。
- data-id: seata-client.yaml
group: mall
refresh: true
5. 需求场景
目前我们有账户、商品、订单三个服务,我们来测试一下这样的场景,在订单服务新增创建订单的方法,创建订单时,同时调用商品服务扣减库存方法和调用账户服务扣减余额方法,交互时序图如下。
6. 需求实现
6.1. 商品服务新增扣减库存方法。
6.1.1. mall-product-center-client 工程
ProductFeignClient 新增的代码如下。
@Operation(summary = "根据商品code扣减数量")
@PostMapping("/reduceCountByCode")
@Parameters({
@Parameter(name = "productCode",description = "商品code",required = true),
@Parameter(name = "count",description = "数量",required = true)
})
void reduceCountByCode(@RequestParam("productCode") String productCode, @RequestParam("count") Integer count) throws BizException;
ProductClientController 新增的代码如下。
@Override
public void reduceCountByCode(String productCode, Integer count) throws BizException {
this.productExecutor.reduceProductCountByCode(productCode,count);
}
6.1.2. mall-product-center-app 工程
ProductExecutor 新增的代码如下。
/**
* 根据商品code扣减库存
*/
public void reduceProductCountByCode(String productCode, Integer count) throws BizException {
this.productService.reduceProductCountByCode(productCode,count);
}
6.1.3. mall-product-center-domain 工程
ProductService 新增的代码如下。
/**
* 根据商品code扣减库存
*/
void reduceProductCountByCode(String productCode, Integer count) throws BizException;
6.1.4. mall-product-center-infra 工程
ProductServiceImpl 新增的代码如下。
@Override
public void reduceProductCountByCode(String productCode, Integer count) throws BizException {
LambdaQueryWrapper<ProductEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(ProductEntity::getProductCode,productCode);
ProductEntity productEntity = this.getOne(lambdaQueryWrapper);
if(Objects.isNull(productEntity)){
throw ExceptionFactory.bizException("商品信息不存在!");
}
productEntity.setCount(productEntity.getCount()-count);
productEntity.setUpdatedBy(1L);
productEntity.setUpdatedTime(new Date());
this.updateById(productEntity);
}
6.2. 账户服务新增扣减余额方法。
6.2.1. mall-account-center-client 工程
新增 AccountFeignClient、AccountFeignClientFallback、AccountFeignClientFallbackFactory 三个类,项目结构如下。
AccountFeignClient 代码如下。
@FeignClient(name = BaseConstant.Domain.ACCOUNT,fallbackFactory = AccountFeignClientFallbackFactory.class)
public interface AccountFeignClient {
@Operation(summary = "根据账户code扣减余额")
@PostMapping("/reduceAmountByCode")
@Parameters({
@Parameter(name = "accountCode",description = "账户code",required = true),
@Parameter(name = "amount",description = "金额",required = true)
})
void reduceAmountByCode(@RequestParam("accountCode") String accountCode, @RequestParam("amount") BigDecimal amount) throws BizException;
}
其中,BaseConstant.Domain.ACCOUNT 值为:mall-account-service。
AccountFeignClientFallback 代码如下。
@Component
public class AccountFeignClientFallback implements AccountFeignClient {
@Override
public void reduceAmountByCode(String accountCode, BigDecimal amount) throws BizException {
throw ExceptionFactory.bizException("扣减余额失败,方法:reduceCountByCode,参数:accountCode="+accountCode+",amount="+amount);
}
}
AccountFeignClientFallbackFactory 代码如下。
@Component
public class AccountFeignClientFallbackFactory implements FallbackFactory<AccountFeignClient> {
@Override
public AccountFeignClient create(Throwable cause) {
cause.printStackTrace();
return new AccountFeignClientFallback();
}
}
6.2.2. mall-account-center-adapter 工程
在 org.example.account.adapter.client 包路径下,新增 AccountClientController类,代码如下。
@Slf4j
@Tag(name = "account-client端api")
@RestController
public class AccountClientController implements AccountFeignClient {
@Resource
private AccountExecutor accountExecutor;
@Override
public void reduceAmountByCode(String accountCode, BigDecimal amount) throws BizException {
this.accountExecutor.reduceAccountAmountByCode(accountCode,amount);
}
}
6.2.3. mall-account-center-app 工程
在 AccountExecutor 类中,增加根据账户code扣减余额的方法,代码如下。
/**
* 根据账户code扣减余额
*/
public void reduceAccountAmountByCode(String accountCode, BigDecimal amount) throws BizException {
this.accountService.reduceAmountByCode(accountCode,amount);
}
6.2.4. mall-account-center-domain 工程
在 AccountService 类中,增加根据账户code扣减余额的方法,代码如下。
/**
* 根据账户code扣减余额
*/
void reduceAmountByCode(String accountCode, BigDecimal amount) throws BizException;
6.2.5. mall-account-center-infra 工程
在 AccountServiceImpl 类中,增加根据账户code扣减余额的方法,代码如下。
@Override
public void reduceAmountByCode(String accountCode, BigDecimal amount) throws BizException {
LambdaQueryWrapper<AccountEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(AccountEntity::getAccountCode,accountCode);
AccountEntity accountEntity = this.getOne(lambdaQueryWrapper);
if(Objects.isNull(accountEntity)){
throw ExceptionFactory.bizException("账户信息不存在!");
}
accountEntity.setAmount(accountEntity.getAmount().subtract(amount));
accountEntity.setUpdatedBy(1L);
accountEntity.setUpdatedTime(new Date());
this.updateById(accountEntity);
}
6.3. 订单服务新增创建订单方法。
6.3.1. mall-order-center-adapter 工程
在 OrderController 类中,新增创建订单的方法,代码如下。
@Operation(summary = "创建订单")
@PostMapping("/createOrder")
public ResponseResult<String> createOrder(@RequestBody OrderReqDTO orderReqDTO){
log.info("createOrder order, orderReqDTO:{}",orderReqDTO);
this.orderExecutor.createOrder(orderReqDTO);
return ResponseResult.ok("createOrder Order succeed");
}
6.3.2. mall-order-center-app 工程
在 OrderExecutor 类中国,新增创建订单的方法,代码如下。
@Component
public class OrderExecutor {
@Resource
private OrderService orderService;
@Resource
private ProductFeignClient productFeignClient;
@Resource
private AccountFeignClient accountFeignClient;
/**
* 省略其他方法
*/
/**
* 创建订单
*/
public void createOrder(OrderReqDTO orderReqDTO) throws BaseException {
this.addOrder(orderReqDTO);
this.productFeignClient.reduceCountByCode(orderReqDTO.getProductCode(),orderReqDTO.getCount());
this.accountFeignClient.reduceAmountByCode(orderReqDTO.getAccountCode(),orderReqDTO.getAmount());
}
}
6.4. 传统事务
6.4.1. 代码实现
传统事务处理方式,是在 org.example.order.app.executor.OrderExecutor#createOrder 方法上添加 @Transactional 注解,代码如下。
@Transactional
public void createOrder(OrderReqDTO orderReqDTO) throws BaseException {
this.addOrder(orderReqDTO);
this.productFeignClient.reduceCountByCode(orderReqDTO.getProductCode(),orderReqDTO.getCount());
this.accountFeignClient.reduceAmountByCode(orderReqDTO.getAccountCode(),orderReqDTO.getAmount());
}
此时,在 org.example.account.infra.impl.AccountServiceImpl#reduceAccountAmountByCode 方法故意加入异常代码,如下所示。
public void reduceAccountAmountByCode(String accountCode, BigDecimal amount) throws BizException {
int a = 1/0;
LambdaQueryWrapper<AccountEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(AccountEntity::getAccountCode,accountCode);
AccountEntity accountEntity = this.accountService.getOne(lambdaQueryWrapper);
if(Objects.isNull(accountEntity)){
throw ExceptionFactory.bizException("账户信息不存在!");
}
accountEntity.setAmount(accountEntity.getAmount().subtract(amount));
accountEntity.setUpdatedBy(1L);
accountEntity.setUpdatedTime(new Date());
this.accountService.updateById(accountEntity);
}
6.4.2. 功能测试
启动账户模块、商品模块、订单模块的服务。测试前,mall_account 表中,account_code = '1000002' 的记录,账户余额为 1000.00;mall_product 表中,product_code='100001' 的记录,库存数量为 10;mall_order 表中记录为空。
此时,前端输入以下参数发起请求,则出现结果报错。
正是 org.example.account.infra.impl.AccountServiceImpl#reduceAccountAmountByCode 方报的错。
此时查看账户金额,仍未扣减。
查看订单数据,依旧为空。
查看商品库存,数量却已扣减为 9。
上面的测试结果,数据一致性已经丢失。
6.5. AT 模式
6.5.1. 代码实现
分别在 mall-account、mall-product、mall-order 数据库中,添加 undo_log 表,sql 脚本如下。
-- 注意此处0.7.0+ 增加字段 context
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,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
Seata 默认的就是 AT 模式,我们只需要将 org.example.order.app.executor.OrderExecutor#createOrder 方法上的 @Transactional 注解改成 @GlobalTransactional 注解即可,如下所示。
@GlobalTransactional
public void createOrder(OrderReqDTO orderReqDTO) throws BaseException {
this.addOrder(orderReqDTO);
this.productFeignClient.reduceCountByCode(orderReqDTO.getProductCode(),orderReqDTO.getCount());
this.accountFeignClient.reduceAmountByCode(orderReqDTO.getAccountCode(),orderReqDTO.getAmount());
}
6.5.2. 功能测试
先将 mall_product 表中 product_code='100001' 的记录,库存数量改回 10。
重启订单服务,前端再用以下参数发起请求,同样出现结果报错。
此时查看账户金额,仍未扣减。
查看订单数据,依旧为空。
查看商品库存,数量仍旧为 10。
上面的测试结果,数据一致性始终保持一致。
undo_log 表的作用是,一阶段插入回滚日志,二阶段提交时异步删除回归日志。
可以在 reduceAmountByCode 方法上设置断点,查看 undo_log 的执行情况。
当程序执行到方法断点时,mall-product、mall-order 数据库中的 undo_log 表,都会被插入回滚日志,如下所示。
当方法执行完,则 undo_log 表的回滚日志将被删除。
6.6. TCC 模式
TCC 需要为每个事务方法增加 commit 和 cancel 的方法。
6.6.1. 代码实现
6.6.1.1. 账户模块扣减余额方法实现
6.6.1.1.1. mall-account-center-domain 工程
创建 org.example.account.domain.action 包目录,在该目录下创建 AccountTccAction 接口,代码如下。
@LocalTCC
public interface AccountTccAction {
/**
* 根据账户code扣减余额
*/
@TwoPhaseBusinessAction(name = "tcc-reduce-amount-by-code", commitMethod = "reduceAmountByCodeCommit", rollbackMethod = "reduceAmountByCodeCancel")
void reduceAmountByCode(@BusinessActionContextParameter(paramName = "accountCode") String accountCode, @BusinessActionContextParameter(paramName = "amount") BigDecimal amount);
/**
* 根据账户code扣减余额-提交
*/
boolean reduceAmountByCodeCommit(BusinessActionContext businessActionContext);
/**
* 根据商品code扣减库存-回滚
*/
boolean reduceAmountByCodeCancel(BusinessActionContext businessActionContext);
}
6.6.1.1.2. mall-account-center-infra 工程
创建 AccountTccAction 接口的实现类 AccountTccActionImpl,代码如下。
@Slf4j
@Component
public class AccountTccActionImpl implements AccountTccAction {
@Resource
private AccountService accountService;
@Override
public void reduceAmountByCode(String accountCode, BigDecimal amount) {
this.accountService.reduceAmountByCode(accountCode,amount);
}
@Override
public boolean reduceAmountByCodeCommit(BusinessActionContext businessActionContext) {
String accountCode = businessActionContext.getActionContext("accountCode")+"";
BigDecimal amount = new BigDecimal(businessActionContext.getActionContext("amount") + "");
log.info("reduceAmountByCodeCommit--------accountCode={},amount={}", accountCode,amount);
return true;
}
@Override
public boolean reduceAmountByCodeCancel(BusinessActionContext businessActionContext) {
String accountCode = businessActionContext.getActionContext("accountCode")+"";
BigDecimal amount = new BigDecimal(businessActionContext.getActionContext("amount") + "");
log.info("reduceAmountByCodeCancel--------accountCode={},amount={}", accountCode,amount);
this.accountService.reduceAmountByCode(accountCode,amount.negate());
return true;
}
}
6.6.1.1.3. mall-account-center-client 工程
给 AccountFeignClient 接口添加方法如下。
@Operation(summary = "根据账户code扣减余额-TCC")
@PostMapping("/reduceAmountByCodeTcc")
@Parameters({
@Parameter(name = "accountCode",description = "账户code",required = true),
@Parameter(name = "amount",description = "金额",required = true)
})
void reduceAmountByCodeTcc(@RequestParam("accountCode") String accountCode, @RequestParam("amount") BigDecimal amount) throws BizException;
AccountFeignClientFallback 添加方法如下。
@Override
public void reduceAmountByCodeTcc(String accountCode, BigDecimal amount) throws BizException {
throw ExceptionFactory.bizException("reduceAmountByCodeTcc-扣减余额失败,方法:reduceCountByCode,参数:accountCode="+accountCode+",amount="+amount);
}
6.6.1.1.4. mall-account-center-adapter 工程
给 AccountClientController 添加方法如下。
@Override
public void reduceAmountByCodeTcc(String accountCode, BigDecimal amount) throws BizException {
this.accountExecutor.reduceAccountAmountByCodeTcc(accountCode,amount);
}
6.6.1.1.5. mall-account-center-app工程
给 AccountExecutor 添加方法如下。
@Component
public class AccountExecutor {
@Resource
private AccountService accountService;
@Resource
private AccountTccAction accountTccAction;
/**
* 省略其他方法
*/
/**
* 根据账户code扣减余额-TCC
*/
public void reduceAccountAmountByCodeTcc(String accountCode, BigDecimal amount) throws BizException {
this.accountTccAction.reduceAmountByCode(accountCode,amount);
}
}
6.6.1.2. 商品模块扣减库存方法实现
6.6.1.2.1. mall-product-center-domain 工程
创建 org.example.product.domain.action 包目录,在该目录下创建 ProductTccAction 接口,代码如下。
@LocalTCC
public interface ProductTccAction {
/**
* 根据商品code扣减库存
*/
@TwoPhaseBusinessAction(name = "tcc-reduce-count-by-code", commitMethod = "reduceCountByCodeCommit", rollbackMethod = "reduceCountByCodeCancel")
void reduceCountByCode(@BusinessActionContextParameter(paramName = "productCode") String productCode, @BusinessActionContextParameter(paramName = "count") Integer count);
/**
* 根据商品code扣减库存-提交
*/
boolean reduceCountByCodeCommit(BusinessActionContext businessActionContext);
/**
* 根据商品code扣减库存-回滚
*/
boolean reduceCountByCodeCancel(BusinessActionContext businessActionContext);
}
6.6.1.2.2. mall-product-center-infra 工程
创建 ProductTccAction 接口的实现类 ProductTccActionImpl,代码如下。
@Slf4j
@Component
public class ProductTccActionImpl implements ProductTccAction {
@Resource
private ProductService productService;
@Override
public void reduceCountByCode(String productCode, Integer count) {
this.productService.reduceProductCountByCode(productCode,count);
}
@Override
public boolean reduceCountByCodeCommit(BusinessActionContext businessActionContext) {
String productCode = businessActionContext.getActionContext("productCode")+"";
Integer count = Integer.getInteger(businessActionContext.getActionContext("count") + "");
log.info("reduceCountByCodeCommit--------productCode={},count={}", productCode,count);
return true;
}
@Override
public boolean reduceCountByCodeCancel(BusinessActionContext businessActionContext) {
String productCode = businessActionContext.getActionContext("productCode")+"";
Integer count = Integer.valueOf(businessActionContext.getActionContext("count") + "");
log.info("reduceCountByCodeCancel--------productCode={},count={}", productCode,count);
this.productService.reduceProductCountByCode(productCode,-count);
return true;
}
}
6.6.1.2.3. mall-product-center-client 工程
给 ProductFeignClient 接口添加方法如下。
@Operation(summary = "根据商品code扣减数量-TCC")
@PostMapping("/reduceCountByCodeTcc")
@Parameters({
@Parameter(name = "productCode",description = "商品code",required = true),
@Parameter(name = "count",description = "数量",required = true)
})
void reduceCountByCodeTcc(@RequestParam("productCode") String productCode, @RequestParam("count") Integer count) throws BizException;
ProductFeignClientFallback 添加方法如下。
@Override
public void reduceCountByCodeTcc(String productCode, Integer count) throws BizException {
throw ExceptionFactory.bizException("reduceCountByCodeTcc-扣减库存失败,方法:reduceCountByCode,参数:productCode="+productCode+",count="+count);
}
6.6.1.2.4. mall-product-center-adapter 工程
给 ProductClientController 添加方法如下。
@Override
public void reduceCountByCodeTcc(String productCode, Integer count) throws BizException {
this.productExecutor.reduceProductCountByCodeTcc(productCode,count);
}
6.6.1.2.5. mall-product-center-app工程
给 ProductExecutor 添加方法如下。
@Component
public class ProductExecutor {
@Resource
private ProductService productService;
@Resource
private ProductTccAction productTccAction;
/**
* 省略其他方法
*/
/**
* 根据商品code扣减库存-TCC
*/
public void reduceProductCountByCodeTcc(String productCode, Integer count) throws BizException {
this.productTccAction.reduceCountByCode(productCode,count);
}
}
6.6.1.3. 订单模块创建订单方法实现
6.6.1.3.1. mall-order-center-domain 工程
创建 org.example.order.domain.action 包目录,在该目录下创建 OrderTccAction 接口,代码如下。
@LocalTCC
public interface OrderTccAction {
/**
* 创建订单
*/
@TwoPhaseBusinessAction(name = "tcc-create-order", commitMethod = "createOrderCommit", rollbackMethod = "createOrderCancel")
void createOrder(OrderReqDTO orderReqDTO,@BusinessActionContextParameter(paramName = "id") Long id) throws BaseException;
/**
* 创建订单-提交
*/
boolean createOrderCommit(BusinessActionContext businessActionContext);
/**
* 创建订单-回滚
*/
boolean createOrderCancel(BusinessActionContext businessActionContext);
}
6.6.1.3.2. mall-order-center-infra 工程
创建 OrderTccAction 接口的实现类 OrderTccActionImpl,代码如下。
@Slf4j
@Component
public class OrderTccActionImpl implements OrderTccAction {
@Resource
private OrderService orderService;
@Override
public void createOrder(OrderReqDTO orderReqDTO, Long id) throws BaseException {
this.orderService.add(orderReqDTO,id);
}
@Override
public boolean createOrderCommit(BusinessActionContext businessActionContext) {
Long id = Long.parseLong(businessActionContext.getActionContext("id") + "");
log.info("createOrderCommit--------id={}",id);
return true;
}
@Override
public boolean createOrderCancel(BusinessActionContext businessActionContext) {
Long id = Long.parseLong(businessActionContext.getActionContext("id") + "");
log.info("createOrderCancel--------id={}",id);
return this.orderService.removeById(id);
}
}
6.6.1.3.3. mall-order-center-adapter 工程
在 OrderController 控制器中,将 createOrder 接口调用创建订单的方法改为调用 createOrderTcc,。
@Operation(summary = "创建订单")
@PostMapping("/createOrder")
public ResponseResult<String> createOrder(@RequestBody OrderReqDTO orderReqDTO){
log.info("createOrder order, orderReqDTO:{}",orderReqDTO);
this.orderExecutor.createOrderTcc(orderReqDTO);
return ResponseResult.ok("createOrder Order succeed");
}
6.6.1.3.4. mall-order-center-app工程
给 OrderExecutor 添加方法如下。
@Component
public class OrderExecutor {
@Resource
private OrderService orderService;
@Resource
private OrderTccAction orderTccAction;
@Resource
private ProductFeignClient productFeignClient;
@Resource
private AccountFeignClient accountFeignClient;
/**
* 省略其他方法
*/
/**
* 创建订单-TCC
*/
@GlobalTransactional
public void createOrderTcc(OrderReqDTO orderReqDTO) throws BaseException {
Long id = IdWorker.getId();
this.orderTccAction.createOrder(orderReqDTO,id);
this.productFeignClient.reduceCountByCodeTcc(orderReqDTO.getProductCode(),orderReqDTO.getCount());
this.accountFeignClient.reduceAmountByCodeTcc(orderReqDTO.getAccountCode(),orderReqDTO.getAmount());
int a = 1/0;
}
}
注意:这里将“int a = 1/0;”异常代码放在 createOrderTcc 方法中,移除 org.example.account.infra.impl.AccountServiceImpl#reduceAmountByCode 方法中的“int a = 1/0;”。
6.6.2. 功能测试
启动账户模块、商品模块、订单模块的服务。测试前,mall_account 表中,account_code = '1000002' 的记录,账户余额为 1000.00;mall_product 表中,product_code='100001' 的记录,库存数量为 10;mall_order 表中记录为空。
前端输入以下参数发起请求,则出现结果报错。
报错日志如下所示。
查询账户表如下,账户余额为 1000.00。
查询商品表如下,库存数量仍为 10。
查询订单表如下,记录仍为空。
sql 执行日志如下。
从上面的测试结果和 sql 执行日志中可以看出,TCC 事务提交失败时,通过补偿保证数据的一致性。TCC 与 AT 的回滚区别就在于,AT 是从 undo_log 表中的镜像进行回滚;而 TCC 是通过我们自定义的方法(reduceAmountByCodeCancel、reduceCountByCodeCancel、createOrderCancel)进行回滚。
6.6.3. 幂等、悬挂和空回滚问题处理
TCC 模式中存在幂等、悬挂和空回滚三大问题。在 Seata1.5.1 之前的版本,需要开发者自己考虑解决方法进行处理,借助 undo_log 表或 redis 都可以处理。在 Seata1.5.1 版本开始,Seata 已经解提供了幂等、悬挂和空回滚问题的解决方案,需要在 @TwoPhaseBusinessAction 上增加 useTCCFence = true 属性,同时在对应数据库中增加 tcc_fence_log 表。tcc_fence_log 表的 sql 脚本如下。
CREATE TABLE IF NOT EXISTS `tcc_fence_log`
(
`xid` VARCHAR(128) NOT NULL COMMENT 'global id',
`branch_id` BIGINT NOT NULL COMMENT 'branch id',
`action_name` VARCHAR(64) NOT NULL COMMENT 'action name',
`status` TINYINT NOT NULL COMMENT 'status(tried:1;committed:2;rollbacked:3;suspended:4)',
`gmt_create` DATETIME(3) NOT NULL COMMENT 'create time',
`gmt_modified` DATETIME(3) NOT NULL COMMENT 'update time',
PRIMARY KEY (`xid`, `branch_id`),
KEY `idx_gmt_modified` (`gmt_modified`),
KEY `idx_status` (`status`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
我们使用的是 Spring Cloud Alibaba 2022.0.0.0 版本,其中的 Seata 版本为 1.7.0,通过使用 useTCCFence = true 属性和增加 tcc_fence_log 表即可。
6.7. Saga 模式
在 Seata 中,目前 Saga 模式是使用状态机方式来实现的。因此,我们下面的下单流程,使用状态机方式实现。
6.7.1. 代码实现
6.7.1.1. 账户模块扣减余额方法实现
6.7.1.1.1. mall-account-center-domain工程
在 org.example.account.domain.action 目录下添加 AccountSagaAction 类。代码如下。
public interface AccountSagaAction {
/**
* 根据账户code扣减余额
*/
boolean reduceAmountByCode(String accountCode, BigDecimal amount);
/**
* 根据账户code扣减余额-方法补偿
*/
boolean compensateReduceAmountByCode(String accountCode, BigDecimal amount);
}
6.7.1.1.2. mall-account-center-infra工程
在 org.example.account.infra.impl 目录下添加 AccountSagaActionImpl 类。代码如下。
@Slf4j
@Service
public class AccountSagaActionImpl implements AccountSagaAction {
@Resource
private AccountService accountService;
@Override
public boolean reduceAmountByCode(String accountCode, BigDecimal amount) {
log.info("reduceAmountByCode--------accountCode={},amount={}", accountCode,amount);
this.accountService.reduceAmountByCode(accountCode,amount);
return true;
}
@Override
public boolean compensateReduceAmountByCode(String accountCode, BigDecimal amount) {
log.info("compensateReduceAmountByCode--------accountCode={},amount={}", accountCode,amount);
this.accountService.reduceAmountByCode(accountCode,amount.negate());
return true;
}
}
6.7.1.1.3. mall-account-center-client 工程
给 AccountFeignClient 接口添加方法如下。
@Operation(summary = "根据账户code扣减余额-Saga")
@PostMapping("/reduceAmountByCodeSaga")
@Parameters({
@Parameter(name = "accountCode",description = "账户code",required = true),
@Parameter(name = "amount",description = "金额",required = true)
})
void reduceAmountByCodeSaga(@RequestParam("accountCode") String accountCode, @RequestParam("amount") BigDecimal amount) throws BizException;
@Operation(summary = "补偿-根据账户code扣减余额-Saga")
@PostMapping("/compensateReduceAmountByCodeSaga")
@Parameters({
@Parameter(name = "accountCode",description = "账户code",required = true),
@Parameter(name = "amount",description = "金额",required = true)
})
void compensateReduceAmountByCodeSaga(@RequestParam("accountCode") String accountCode, @RequestParam("amount") BigDecimal amount) throws BizException;
AccountFeignClientFallback 添加方法如下。
@Override
public void reduceAmountByCodeSaga(String accountCode, BigDecimal amount) throws BizException {
throw ExceptionFactory.bizException("reduceAmountByCodeSaga-扣减余额失败,方法:reduceCountByCode,参数:accountCode="+accountCode+",amount="+amount);
}
@Override
public void compensateReduceAmountByCodeSaga(String accountCode, BigDecimal amount) throws BizException {
throw ExceptionFactory.bizException("compensateReduceAmountByCodeSaga-补偿扣减余额失败,方法:reduceCountByCode,参数:accountCode="+accountCode+",amount="+amount);
}
6.7.1.1.4. mall-account-center-adapter 工程
给 AccountClientController 添加方法如下。
@Override
public void reduceAmountByCodeSaga(String accountCode, BigDecimal amount) throws BizException {
this.accountExecutor.reduceAccountAmountByCodeSaga(accountCode,amount);
}
@Override
public void compensateReduceAmountByCodeSaga(String accountCode, BigDecimal amount) throws BizException {
this.accountExecutor.compensateReduceAccountAmountByCodeSaga(accountCode,amount);
}
6.7.1.1.5. mall-account-center-app工程
给 AccountExecutor 添加方法如下。
@Component
public class AccountExecutor {
@Resource
private AccountService accountService;
@Resource
private AccountTccAction accountTccAction;
@Resource
private AccountSagaAction accountSagaAction;
/**
* 省略其他方法
*/
/**
* 根据账户code扣减余额-Saga
*/
public void reduceAccountAmountByCodeSaga(String accountCode, BigDecimal amount) throws BizException {
this.accountSagaAction.reduceAmountByCode(accountCode,amount);
}
/**
* 补偿-根据账户code扣减余额-Saga
*/
public void compensateReduceAccountAmountByCodeSaga(String accountCode, BigDecimal amount) throws BizException {
this.accountSagaAction.compensateReduceAmountByCode(accountCode,amount);
}
}
6.7.1.2. 商品模块扣减库存方法实现
6.7.1.2.1. mall-product-center-domain工程
在 org.example.product.domain.action 目录下添加 ProductSagaAction 类。代码如下。
public interface ProductSagaAction {
/**
* 根据商品code扣减库存
*/
void reduceCountByCode(String productCode, Integer count);
/**
* 根据商品code扣减库存-方法补偿
*/
void compensateReduceCountByCode(String productCode, Integer count);
}
6.7.1.2.2. mall-product-center-infra工程
在 org.example.product.infra.impl 目录下添加 ProductSagaActionImpl 类。代码如下。
@Slf4j
@Service
public class ProductSagaActionImpl implements ProductSagaAction {
@Resource
private ProductService productService;
@Override
public void reduceCountByCode(String productCode, Integer count) {
log.info("reduceCountByCode--------productCode={},count={}", productCode,count);
this.productService.reduceProductCountByCode(productCode,count);
}
@Override
public void compensateReduceCountByCode(String productCode, Integer count) {
log.info("reduceCountByCodeCancel--------productCode={},count={}", productCode,count);
this.productService.reduceProductCountByCode(productCode,-count);
}
}
6.7.1.2.3. mall-product-center-client 工程
给 ProductFeignClient 接口添加方法如下。
@Operation(summary = "根据商品code扣减数量-Saga")
@PostMapping("/reduceCountByCodeSaga")
@Parameters({
@Parameter(name = "productCode",description = "商品code",required = true),
@Parameter(name = "count",description = "数量",required = true)
})
void reduceCountByCodeSaga(@RequestParam("productCode") String productCode, @RequestParam("count") Integer count) throws BizException;
@Operation(summary = "补偿-根据商品code扣减数量-Saga")
@PostMapping("/compensateReduceCountByCodeSaga")
@Parameters({
@Parameter(name = "productCode",description = "商品code",required = true),
@Parameter(name = "count",description = "数量",required = true)
})
void compensateReduceCountByCodeSaga(@RequestParam("productCode") String productCode, @RequestParam("count") Integer count) throws BizException;
ProductFeignClientFallback 添加方法如下。
@Override
public void reduceCountByCodeSaga(String productCode, Integer count) throws BizException {
throw ExceptionFactory.bizException("reduceCountByCodeSaga-扣减库存失败,方法:reduceCountByCode,参数:productCode="+productCode+",count="+count);
}
@Override
public void compensateReduceCountByCodeSaga(String productCode, Integer count) throws BizException {
throw ExceptionFactory.bizException("compensateReduceCountByCodeSaga-补偿扣减库存失败,方法:reduceCountByCode,参数:productCode="+productCode+",count="+count);
}
6.7.1.2.4. mall-product-center-adapter 工程
给 ProductClientController 添加方法如下。
@Override
public void reduceCountByCodeSaga(String productCode, Integer count) throws BizException {
this.productExecutor.reduceProductCountByCodeSaga(productCode,count);
}
@Override
public void compensateReduceCountByCodeSaga(String productCode, Integer count) throws BizException {
this.productExecutor.compensateReduceProductCountByCodeSaga(productCode,count);
}
6.7.1.2.5. mall-product-center-app工程
给 ProductExecutor 添加方法如下。
@Component
public class ProductExecutor {
@Resource
private ProductService productService;
@Resource
private ProductTccAction productTccAction;
@Resource
private ProductSagaAction productSagaAction;
/**
* 省略其他方法
*/
/**
* 根据商品code扣减库存-Saga
*/
public void reduceProductCountByCodeSaga(String productCode, Integer count) throws BizException {
this.productSagaAction.reduceCountByCode(productCode,count);
}
/**
* 补偿-根据商品code扣减库存-Saga
*/
public void compensateReduceProductCountByCodeSaga(String productCode, Integer count) throws BizException {
this.productSagaAction.compensateReduceCountByCode(productCode,count);
}
}
6.7.1.3. 订单模块创建订单方法实现
6.7.1.3.1. mall-order-center-domain工程
在 org.example.order.domain.action 目录下添加 OrderSagaAction 类。代码如下。
public interface OrderSagaAction {
/**
* 创建订单
*/
boolean createOrder(OrderReqDTO orderReqDTO,Long id);
/**
* 创建订单-方法补偿
*/
boolean compensateCreateOrder(Long id);
}
6.7.1.3.2. mall-order-center-infra工程
在 org.example.order.infra.impl 目录下添加 OrderSagaActionImpl 类。代码如下。
@Slf4j
@Service("orderSagaAction")
public class OrderSagaActionImpl implements OrderSagaAction {
@Resource
private OrderService orderService;
@Override
public boolean createOrder(OrderReqDTO orderReqDTO, Long id) {
log.info("createOrder--------orderReqDTO={},id={}", JSON.toJSONString(orderReqDTO),id);
if(Objects.isNull(orderReqDTO)){
throw ExceptionFactory.bizException("订单参数为空!");
}
this.orderService.add(orderReqDTO,id);
return true;
}
@Override
public boolean compensateCreateOrder(Long id) {
log.info("createOrderCancel--------id={}",id);
this.orderService.removeById(id);
return true;
}
}
6.7.1.3.3. mall-order-center-adapter 工程
在 OrderController 控制器中,将 createOrder 接口调用创建订单的方法改为调用 createOrderSaga,。
@Operation(summary = "创建订单")
@PostMapping("/createOrder")
public ResponseResult<String> createOrder(@RequestBody OrderReqDTO orderReqDTO){
log.info("createOrder order, orderReqDTO:{}",orderReqDTO);
this.orderExecutor.createOrderSaga(orderReqDTO);
return ResponseResult.ok("createOrder Order succeed");
}
6.7.1.3.4. mall-order-center-app工程
在 org.example.order.app 包目录下 ,创建 config 包目录,在 config 包目录下创建名称为 StatemachineEngineConfig 的配置类,代码如下。
@Configuration
public class StatemachineEngineConfig {
@Value("${seata.tx-service-group}")
private String txServiceGroup;
@Resource
private DataSource dataSource;
@Bean("sagaThreadExecutor")
ThreadPoolExecutor sagaThreadExecutor(){
ThreadPoolExecutorFactoryBean threadPoolExecutorFactoryBean = new ThreadPoolExecutorFactoryBean();
threadPoolExecutorFactoryBean.setThreadNamePrefix("SAGA_ASYNC_EXE_");
threadPoolExecutorFactoryBean.setCorePoolSize(1);
threadPoolExecutorFactoryBean.setMaxPoolSize(20);
threadPoolExecutorFactoryBean.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return new ThreadPoolExecutor(1, 20, 3, TimeUnit.SECONDS, new LinkedBlockingDeque<>(), threadPoolExecutorFactoryBean, new ThreadPoolExecutor.CallerRunsPolicy());
}
@Bean("dbStateMachineConfig")
DbStateMachineConfig dbStateMachineConfig(){
DbStateMachineConfig dbStateMachineConfig = new DbStateMachineConfig();
dbStateMachineConfig.setDataSource(dataSource);
dbStateMachineConfig.setResources(new String[]{"saga/statelang/*.json"});
dbStateMachineConfig.setEnableAsync(true);
dbStateMachineConfig.setThreadPoolExecutor(sagaThreadExecutor());
dbStateMachineConfig.setApplicationId(BaseConstant.Domain.PRODUCT);
dbStateMachineConfig.setTxServiceGroup(txServiceGroup);
dbStateMachineConfig.setSagaBranchRegisterEnable(false);
dbStateMachineConfig.setSagaJsonParser("jackson");
dbStateMachineConfig.setSagaRetryPersistModeUpdate(false);
dbStateMachineConfig.setSagaCompensatePersistModeUpdate(false);
return dbStateMachineConfig;
}
@Bean("stateMachineEngine")
ProcessCtrlStateMachineEngine stateMachineEngine(){
ProcessCtrlStateMachineEngine processCtrlStateMachineEngine = new ProcessCtrlStateMachineEngine();
processCtrlStateMachineEngine.setStateMachineConfig(dbStateMachineConfig());
return processCtrlStateMachineEngine;
}
@Bean
StateMachineEngineHolder stateMachineEngineHolder(){
StateMachineEngineHolder stateMachineEngineHolder = new StateMachineEngineHolder();
stateMachineEngineHolder.setStateMachineEngine(stateMachineEngine());
return stateMachineEngineHolder;
}
}
给 OrderExecutor 添加调用状态机创建订单的方法,代码如下。
@Component
public class OrderExecutor {
@Resource
private OrderService orderService;
@Resource
private OrderTccAction orderTccAction;
@Resource
private ProductFeignClient productFeignClient;
@Resource
private AccountFeignClient accountFeignClient;
/**
* 省略其他方法
*/
/**
* 创建订单-Saga
*/
@GlobalTransactional
public void createOrderSaga(OrderReqDTO orderReqDTO) throws BaseException {
StateMachineEngine stateMachineEngine = StateMachineEngineHolder.getStateMachineEngine();
Long id = IdWorker.getId();
Map<String, Object> startParams = new HashMap<>(6);
startParams.put("id", id);
startParams.put("productCode", orderReqDTO.getProductCode());
startParams.put("count", orderReqDTO.getCount());
startParams.put("accountCode", orderReqDTO.getAccountCode());
startParams.put("amount", orderReqDTO.getAmount());
startParams.put("orderReqDTO", orderReqDTO);
StateMachineInstance inst = stateMachineEngine.start("createOrderFlow",null,startParams);
log.info("saga transaction commit succeed. XID: {}",inst.getId());
if(!ExecutionStatus.SU.equals(inst.getStatus())){
throw ExceptionFactory.bizException("saga transaction execute failed. XID: " + inst.getId());
}
}
}
这里涉及到在订单服务远程调用账户服务、商品服务,由于 AccountFeignClient、ProductFeignClient 是接口,如果将它们作为 Bean,直接配置到状态机的 ServiceName 中,Saga 将会找不到 AccountFeignClient、ProductFeignClient 对应的 Bean。因此,我们需要对 AccountFeignClient、ProductFeignClient 中参与 Saga 事务的方法进行进行间接调用,处理方法如下。
添加 org.example.order.app.action 目录,创建 AccountSagaAction、ProductSagaAction 接口,
代码如下。
public interface AccountSagaAction {
/**
* 根据账户code扣减余额
*/
boolean reduceAmountByCode(String accountCode, BigDecimal amount);
/**
* 根据账户code扣减余额-方法补偿
*/
boolean compensateReduceAmountByCode(String accountCode, BigDecimal amount);
}
public interface ProductSagaAction {
/**
* 根据商品code扣减库存
*/
boolean reduceCountByCode(String productCode, Integer count);
/**
* 根据商品code扣减库存-方法补偿
*/
boolean compensateReduceCountByCode(String productCode, Integer count);
}
添加 org.example.order.app.action.impl 目录,分别创建 AccountSagaAction、ProductSagaAction 接口的实现类 AccountSagaActionImpl、ProductSagaActionImpl,代码如下。
@Slf4j
@Service("accountSagaAction")
public class AccountSagaActionImpl implements AccountSagaAction {
@Resource
private AccountFeignClient accountFeignClient;
@Override
public boolean reduceAmountByCode(String accountCode, BigDecimal amount) {
log.info("reduceAmountByCode--------accountCode={},amount={}", accountCode,amount);
this.accountFeignClient.reduceAmountByCode(accountCode,amount);
return true;
}
@Override
public boolean compensateReduceAmountByCode(String accountCode, BigDecimal amount) {
log.info("compensateReduceAmountByCode--------accountCode={},amount={}", accountCode,amount);
this.accountFeignClient.compensateReduceAmountByCodeSaga(accountCode,amount);
return true;
}
}
@Slf4j
@Service("productSagaAction")
public class ProductSagaActionImpl implements ProductSagaAction {
@Resource
private ProductFeignClient productFeignClient;
@Override
public boolean reduceCountByCode(String productCode, Integer count) {
log.info("reduceCountByCode--------productCode={},count={}", productCode,count);
this.productFeignClient.reduceCountByCodeSaga(productCode,count);
return true;
}
@Override
public boolean compensateReduceCountByCode(String productCode, Integer count) {
log.info("reduceCountByCodeCancel--------productCode={},count={}", productCode,count);
this.productFeignClient.compensateReduceCountByCodeSaga(productCode,count);
return true;
}
}
这样,就可以将 accountSagaAction、productSagaAction 作为 Bean 的名称,配置到状态的 ServiceName 中了。
6.7.2. 状态机设计
在 mall-order-center-start 工程的 resources 目录下,创建“saga\statelang”目录, 在“resources\saga\statelang”目录下,创建名称为 saga-created-order-flow.json 的状态机文件,内容如下。
{
"nodes": [
{
"type": "node",
"size": "180*48",
"shape": "flow-capsule",
"color": "#722ED1",
"label": "compensateReduceAmount",
"stateId": "reduce-amount-compensation",
"stateType": "Compensation",
"stateProps": {
"ServiceName": "accountSagaAction",
"ServiceMethod": "compensateReduceAmountByCode",
"Input": [
"$.[accountCode]",
"$.[amount]"
],
"Output": {
"compensateReduceAmountResult": "$.#root"
},
"Status": {
"#root == true": "SU",
"#root == false": "FA",
"$Exception{java.lang.Throwable}": "UN"
},
"Retry": []
},
"x": 118.125,
"y": -265.5,
"id": "77883822",
"index": 17
},
{
"type": "node",
"size": "39*39",
"shape": "flow-circle",
"color": "red",
"label": "Catch",
"stateId": "create-order-catch",
"stateType": "Catch",
"x": 379.125,
"y": 68.5,
"id": "99285853",
"index": 11
},
{
"type": "node",
"size": "72*72",
"shape": "flow-circle",
"color": "#FA8C16",
"label": "Start",
"stateId": "create-order-start",
"stateType": "Start",
"stateProps": {
"StateMachine": {
"Name": "createOrderFlow",
"Comment": "create order flow,reduce count,reduce amount,create order",
"Version": "0.0.1"
}
},
"x": 323.625,
"y": -565,
"id": "be648d4f",
"index": 0
},
{
"type": "node",
"size": "110*48",
"shape": "flow-rect",
"color": "#1890FF",
"label": "reduceCount",
"stateId": "reduce-count-service-task",
"stateType": "ServiceTask",
"stateProps": {
"ServiceName": "productSagaAction",
"ServiceMethod": "reduceCountByCode",
"Input": [
"$.[productCode]",
"$.[count]"
],
"Output": {
"reduceCountResult": "$.#root"
},
"Status": {
"#root == true": "SU",
"#root == false": "FA",
"$Exception{java.lang.Throwable}": "UN"
},
"Retry": []
},
"x": 323.625,
"y": -447.5,
"id": "c9a51329",
"index": 1
},
{
"type": "node",
"size": "39*39",
"shape": "flow-circle",
"color": "red",
"label": "Catch",
"stateId": "reduce-amount-catch",
"stateType": "Catch",
"x": 380.125,
"y": -182.5,
"id": "167e3e70",
"index": 12
},
{
"type": "node",
"size": "110*48",
"shape": "flow-rect",
"color": "#1890FF",
"label": "reduceAmount",
"stateId": "reduce-amount-service-task",
"stateType": "ServiceTask",
"stateProps": {
"ServiceName": "accountSagaAction",
"ServiceMethod": "reduceAmountByCode",
"Input": [
"$.[accountCode]",
"$.[amount]"
],
"Output": {
"reduceAmountResult": "$.#root"
},
"Status": {
"#root == true": "SU",
"#root == false": "FA",
"$Exception{java.lang.Throwable}": "UN"
},
"Retry": []
},
"x": 324.625,
"y": -195,
"id": "bbd4a3d6",
"index": 18
},
{
"type": "node",
"size": "80*72",
"shape": "flow-rhombus",
"color": "#13C2C2",
"label": "Choice",
"stateId": "reduce-amount-choice",
"stateType": "Choice",
"x": 324.625,
"y": -84,
"id": "202e18cb",
"index": 19
},
{
"type": "node",
"size": "110*48",
"shape": "flow-rect",
"color": "#1890FF",
"label": "createOrder",
"stateId": "create-order-service-task",
"stateType": "ServiceTask",
"stateProps": {
"ServiceName": "orderSagaAction",
"ServiceMethod": "createOrder",
"ParameterTypes": [
"org.example.order.dto.req.OrderReqDTO",
"java.lang.Long"
],
"Input": [
{
"accountCode": "$.[orderReqDTO].accountCode",
"productCode": "$.[orderReqDTO].productCode",
"count": "$.[orderReqDTO].count",
"amount": "$.[orderReqDTO].amount"
},
"$.[id]"
],
"Output": {
"createOrderResult": "$.#root"
},
"Status": {
"#root == true": "SU",
"#root == false": "FA",
"$Exception{java.lang.Throwable}": "UN"
},
"Retry": []
},
"x": 324.625,
"y": 48,
"id": "144251b0",
"index": 20
},
{
"type": "node",
"size": "72*72",
"shape": "flow-circle",
"color": "#05A465",
"label": "Succeed",
"stateId": "succeed",
"stateType": "Succeed",
"x": 324.625,
"y": 173,
"id": "efca467d",
"index": 21
},
{
"type": "node",
"size": "80*72",
"shape": "flow-rhombus",
"color": "#13C2C2",
"label": "Choice",
"stateId": "reduce-count-choice",
"stateType": "Choice",
"x": 323.625,
"y": -334,
"id": "efbc1565",
"index": 22
},
{
"type": "node",
"size": "180*48",
"shape": "flow-capsule",
"color": "#722ED1",
"label": "compensateReduceCount",
"stateId": "reduce-count-compensation",
"stateType": "Compensation",
"stateProps": {
"ServiceName": "productSagaAction",
"ServiceMethod": "compensateReduceCountByCode",
"Input": [
"$.[productCode]",
"$.[count]"
],
"Output": {
"compensateReduceCountResult": "$.#root"
},
"Status": {
"#root == true": "SU",
"#root == false": "FA",
"$Exception{java.lang.Throwable}": "UN"
},
"Retry": []
},
"x": 136.125,
"y": -496.5,
"id": "91f70287",
"index": 24
},
{
"type": "node",
"size": "72*72",
"shape": "flow-circle",
"color": "red",
"label": "Fail",
"stateId": "fail",
"stateType": "Fail",
"stateProps": {
"ErrorCode": "PURCHASE_FAILED",
"Message": "purchase failed"
},
"x": 630.625,
"y": -334,
"id": "765ed40d",
"index": 25
},
{
"type": "node",
"size": "110*48",
"shape": "flow-capsule",
"color": "red",
"label": "Compensation\nTrigger",
"stateId": "compensation-trigger",
"stateType": "CompensationTrigger",
"x": 630.625,
"y": -182.5,
"id": "169d41c5",
"index": 26
},
{
"type": "node",
"size": "180*48",
"shape": "flow-capsule",
"color": "#722ED1",
"label": "compensateCreateOrder",
"stateId": "create-order-compensation",
"stateType": "Compensation",
"stateProps": {
"ServiceName": "orderSagaAction",
"ServiceMethod": "compensateCreateOrder",
"Input": [
"$.[id]"
],
"Output": {
"compensateCreateOrderResult": "$.#root"
},
"Status": {
"#root == true": "SU",
"#root == false": "FA",
"$Exception{java.lang.Throwable}": "UN"
},
"Retry": []
},
"x": 108.125,
"y": -36.5,
"id": "bb32be7b"
}
],
"edges": [
{
"source": "bbd4a3d6",
"sourceAnchor": 3,
"target": "77883822",
"targetAnchor": 2,
"id": "ec7ad3aa",
"shape": "flow-polyline-round",
"style": {
"lineDash": "4",
"endArrow": false
},
"type": "Compensation",
"index": 5
},
{
"source": "c9a51329",
"sourceAnchor": 3,
"target": "91f70287",
"targetAnchor": 2,
"id": "02716f5c",
"shape": "flow-polyline-round",
"style": {
"lineDash": "4",
"endArrow": false
},
"type": "Compensation",
"index": 6
},
{
"source": "167e3e70",
"sourceAnchor": 1,
"target": "169d41c5",
"targetAnchor": 3,
"id": "0a2bd221",
"shape": "flow-polyline-round",
"stateProps": {
"Exceptions": [
"java.lang.Throwable"
]
},
"index": 9
},
{
"source": "efbc1565",
"sourceAnchor": 2,
"target": "bbd4a3d6",
"targetAnchor": 0,
"id": "4f1c37ad",
"shape": "flow-polyline-round",
"stateProps": {
"Expression": "[reduceCountResult] == true",
"Default": false
},
"label": "Choice",
"index": 13
},
{
"source": "bbd4a3d6",
"sourceAnchor": 2,
"target": "202e18cb",
"targetAnchor": 0,
"id": "cd1dd5f5",
"shape": "flow-polyline-round",
"index": 14
},
{
"source": "efbc1565",
"sourceAnchor": 1,
"target": "765ed40d",
"targetAnchor": 3,
"id": "8ac9c9e9",
"shape": "flow-polyline-round",
"stateProps": {
"Expression": "[reduceCountResult] == false",
"Default": false
},
"index": 16,
"label": ""
},
{
"source": "be648d4f",
"sourceAnchor": 2,
"target": "c9a51329",
"targetAnchor": 0,
"id": "1bc85138",
"shape": "flow-polyline-round",
"label": "",
"index": 23
},
{
"source": "202e18cb",
"sourceAnchor": 2,
"target": "144251b0",
"targetAnchor": 0,
"id": "3b92ffb2",
"shape": "flow-polyline-round",
"stateProps": {
"Expression": "[reduceAmountResult]==true",
"Default": false
},
"label": "Choice"
},
{
"source": "144251b0",
"sourceAnchor": 2,
"target": "efca467d",
"targetAnchor": 0,
"id": "2c0e194a",
"shape": "flow-polyline-round"
},
{
"source": "144251b0",
"sourceAnchor": 3,
"target": "bb32be7b",
"targetAnchor": 2,
"id": "ed21c808",
"shape": "flow-polyline-round",
"style": {
"lineDash": "4",
"endArrow": false
},
"type": "Compensation"
},
{
"source": "169d41c5",
"sourceAnchor": 0,
"target": "765ed40d",
"targetAnchor": 2,
"id": "2888277a",
"shape": "flow-polyline-round"
},
{
"source": "99285853",
"sourceAnchor": 1,
"target": "169d41c5",
"targetAnchor": 2,
"id": "b211ada4",
"shape": "flow-polyline-round",
"stateProps": {
"Exceptions": [
"java.lang.Throwable"
]
}
},
{
"source": "202e18cb",
"sourceAnchor": 1,
"target": "169d41c5",
"targetAnchor": 2,
"id": "b9c1fe95",
"shape": "flow-polyline-round",
"stateProps": {
"Expression": "[reduceAmountResult]==false",
"Default": false
},
"label": ""
},
{
"source": "c9a51329",
"sourceAnchor": 2,
"target": "efbc1565",
"targetAnchor": 0,
"id": "4912ec0e",
"shape": "flow-polyline-round"
}
]
}
saga-created-order-flow.json 文件对应的状态机效果图如下。
上面的状态机可以使用 官网地址 提供的界面来设计。
6.7.3. 建表 sql 初始化
Saga 模式,需要用到 seata_state_machine_def、seata_state_machine_inst、seata_state_inst 三张表。需要分别在账户模块、商品模块、订单模块的数据库中添加这三张表。下面是初始化三张表的 sql 脚本。
-- -------------------------------- The script used for sage --------------------------------
CREATE TABLE IF NOT EXISTS `seata_state_machine_def`
(
`id` VARCHAR(32) NOT NULL COMMENT 'id',
`name` VARCHAR(128) NOT NULL COMMENT 'name',
`tenant_id` VARCHAR(32) NOT NULL COMMENT 'tenant id',
`app_name` VARCHAR(32) NOT NULL COMMENT 'application name',
`type` VARCHAR(20) COMMENT 'state language type',
`comment_` VARCHAR(255) COMMENT 'comment',
`ver` VARCHAR(16) NOT NULL COMMENT 'version',
`gmt_create` DATETIME(3) NOT NULL COMMENT 'create time',
`status` VARCHAR(2) NOT NULL COMMENT 'status(AC:active|IN:inactive)',
`content` TEXT COMMENT 'content',
`recover_strategy` VARCHAR(16) COMMENT 'transaction recover strategy(compensate|retry)',
PRIMARY KEY (`id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
CREATE TABLE IF NOT EXISTS `seata_state_machine_inst`
(
`id` VARCHAR(128) NOT NULL COMMENT 'id',
`machine_id` VARCHAR(32) NOT NULL COMMENT 'state machine definition id',
`tenant_id` VARCHAR(32) NOT NULL COMMENT 'tenant id',
`parent_id` VARCHAR(128) COMMENT 'parent id',
`gmt_started` DATETIME(3) NOT NULL COMMENT 'start time',
`business_key` VARCHAR(48) COMMENT 'business key',
`start_params` TEXT COMMENT 'start parameters',
`gmt_end` DATETIME(3) COMMENT 'end time',
`excep` BLOB COMMENT 'exception',
`end_params` TEXT COMMENT 'end parameters',
`status` VARCHAR(2) COMMENT 'status(SU succeed|FA failed|UN unknown|SK skipped|RU running)',
`compensation_status` VARCHAR(2) COMMENT 'compensation status(SU succeed|FA failed|UN unknown|SK skipped|RU running)',
`is_running` TINYINT(1) COMMENT 'is running(0 no|1 yes)',
`gmt_updated` DATETIME(3) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `unikey_buz_tenant` (`business_key`, `tenant_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
CREATE TABLE IF NOT EXISTS `seata_state_inst`
(
`id` VARCHAR(48) NOT NULL COMMENT 'id',
`machine_inst_id` VARCHAR(128) NOT NULL COMMENT 'state machine instance id',
`name` VARCHAR(128) NOT NULL COMMENT 'state name',
`type` VARCHAR(20) COMMENT 'state type',
`service_name` VARCHAR(128) COMMENT 'service name',
`service_method` VARCHAR(128) COMMENT 'method name',
`service_type` VARCHAR(16) COMMENT 'service type',
`business_key` VARCHAR(48) COMMENT 'business key',
`state_id_compensated_for` VARCHAR(50) COMMENT 'state compensated for',
`state_id_retried_for` VARCHAR(50) COMMENT 'state retried for',
`gmt_started` DATETIME(3) NOT NULL COMMENT 'start time',
`is_for_update` TINYINT(1) COMMENT 'is service for update',
`input_params` TEXT COMMENT 'input parameters',
`output_params` TEXT COMMENT 'output parameters',
`status` VARCHAR(2) NOT NULL COMMENT 'status(SU succeed|FA failed|UN unknown|SK skipped|RU running)',
`excep` BLOB COMMENT 'exception',
`gmt_updated` DATETIME(3) COMMENT 'update time',
`gmt_end` DATETIME(3) COMMENT 'end time',
PRIMARY KEY (`id`, `machine_inst_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
6.7.4. 功能测试
6.7.4.1. 正常流程测试
启动账户模块、商品模块、订单模块的服务。测试前,mall_account 表中,account_code = '1000002' 的记录,账户余额为 1000.00;mall_product 表中,product_code='100001' 的记录,库存数量为 10;mall_order 表中记录为空。
前端输入以下参数发起请求,则返回成功。
此时,mall_account 表中,account_code = '1000002' 的记录,账户余额为 980.00;
mall_product 表中,product_code='100001' 的记录,库存数量为 9;
mall_order 表中,则插入了一条 account_code = '1000002'、product_code='100001' 的记录。
6.7.4.2. 异常流程测试
在 org.example.order.infra.impl.OrderSagaActionImpl#createOrder 方法中,加入“int a = 1/0;”的异常代码。
前端输入以下参数发起请求,则出现结果报错。
报错的主要日志如下。
INFO 13892 --- [nio-7050-exec-1] o.e.o.infra.impl.OrderSagaActionImpl : createOrder--------orderReqDTO={"accountCode":"1000002","amount":20,"count":1,"productCode":"100001"},id=1700858392476233729
ERROR 13892 --- [nio-7050-exec-1] i.s.s.e.p.h.ServiceTaskStateHandler : <<<<<<<<<<<<<<<<<<<<<< State[create-order-service-task], ServiceName[orderSagaAction], Method[createOrder] Execute failed.
java.lang.ArithmeticException: / by zero
at org.example.order.infra.impl.OrderSagaActionImpl.createOrder(OrderSagaActionImpl.java:34) ~[classes/:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
此时,mall_account 表中,account_code = '1000002' 的记录,账户余额仍为 980.00;
mall_product 表中,product_code='100001' 的记录,库存数量仍为 9;
mall_order 表中,还是只有一条记录。
6.8. XA 模式
6.8.1. 代码实现
XA模式需配置数据源代理,分别给账户模块、商品模块、订单模块添加数据源代理配置。以订单模块为例。在 mall-order-center-infra 工程中新建 org.example.order.infra.config 目录,在该目录下,创建名称为 OrderXADataSourceConfiguration 的配置类。内容如下。
@Configuration
public class ProductXADataSourceConfiguration {
@Value("${spring.datasource.url}")
private String datasourceUrl;
@Value("${spring.datasource.username}")
private String datasourceUsername;
@Value("${spring.datasource.password}")
private String datasourcePassword;
@Bean("dataSourceProxy")
public DataSource dataSource() {
HikariDataSource hikariDataSource = new HikariDataSource();
hikariDataSource.setJdbcUrl(datasourceUrl);
hikariDataSource.setUsername(datasourceUsername);
hikariDataSource.setPassword(datasourcePassword);
// DataSourceProxy for AT mode
//return new DataSourceProxy(hikariDataSource);
// DataSourceProxyXA for XA mode
return new DataSourceProxyXA(hikariDataSource);
}
@Bean("jdbcTemplate")
public JdbcTemplate jdbcTemplate(DataSource dataSourceProxy) {
return new JdbcTemplate(dataSourceProxy);
}
@Bean
public PlatformTransactionManager txManager(DataSource dataSourceProxy) {
return new DataSourceTransactionManager(dataSourceProxy);
}
}
在 seata-client.yaml 中,设置数据源代理模式为 XA。如下所示。
6.8.2. 功能测试
将 OrderController 创建订单的方法,改回如下代码。
@Operation(summary = "创建订单")
@PostMapping("/createOrder")
public ResponseResult<String> createOrder(@RequestBody OrderReqDTO orderReqDTO){
log.info("createOrder order, orderReqDTO:{}",orderReqDTO);
this.orderExecutor.createOrder(orderReqDTO);
return ResponseResult.ok("createOrder Order succeed");
}
org.example.order.app.executor.OrderExecutor#createOrder 方法代码如下。
/**
* 创建订单
*/
@GlobalTransactional
public void createOrder(OrderReqDTO orderReqDTO) throws BaseException {
this.addOrder(orderReqDTO);
this.productFeignClient.reduceCountByCode(orderReqDTO.getProductCode(),orderReqDTO.getCount());
this.accountFeignClient.reduceAmountByCode(orderReqDTO.getAccountCode(),orderReqDTO.getAmount());
int a = 1/0;
}
启动账户模块、商品模块、订单模块的服务。测试前,mall_account 表中,account_code = '1000002' 的记录,账户余额为 1000.00;mall_product 表中,product_code='100001' 的记录,库存数量为 10;mall_order 表中记录为空。
前端输入以下参数发起请求,则出现结果报错。
报错日志如下所示。
再查看 mall_account、mall_product、mall_order 三表的数据,均无变化。
验证是否是 XA 模式的事务。
在 org.example.order.app.executor.OrderExecutor#createOrder 方法上,打上如下断点,前端再次发起请求。
查看 branch_table 表的数据如下。
可以看到,事务分支注册类型为 XA。
7. 总结
本篇先介绍了 Seata Server 的搭建,及 Seata 客户端的整合。接着提出了一个简单的下单需求场景。然后对该下单的需求场景,分别使用 AT、TCC、Saga、XA 四种模式进行实现并测试。对于 AT、TCC、Saga、XA 四种模式。AT 模式实现起来,是四种模式中最简单的模式,也是 Seata 默认的模式。XA 模式与 AT 模式非常相似,只需对数据源做少量代码的配置即可像 AT 模式一样使用。TCC 模式需要业务提供 prepare 、commit 和 rollback 三个的方法,代码入侵较多,但 TCC 模式在几种模式中效率较高,因为TCC 模式在 try 阶段就提交本地事务了。Saga 模式应该是四种模式中使用起来最为复杂,门槛最高的模式了,Saga 模式适合使用在业务流程长、业务流程多,或者参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口的场景。以上四种模式,可以根据实际场景,选择合适额模式进行使用。
基础篇项目代码:链接地址