文章目录
- SeataXA模式
- 整体机制
- 微服务整合SeataXA
- SeataTCC模式
- 什么是TCC
- 以用户下单为例
- Seata TCC 模式
- Seata TCC模式接口改造
- TCC如何控制异常
- 空回滚
- 幂等
- 悬挂
- 微服务整合SeataTCC
- 比较
SeataXA模式
XA协议最主要的作用是就是定义了RM-TM的交互接口,除此之外,还对两阶段提交协议进行了优化。
整体机制
在 Seata 定义的分布式事务框架内,利用事务资源(数据库、消息服务等)对 XA 协议的支持,以 XA 协议的机制来管理分支事务的一种事务模式。
-
执行阶段:
-
可回滚:业务 SQL 操作放在 XA 分支中进行,由资源对 XA 协议的支持来保证 可回滚
-
持久化:XA 分支完成后,执行 XA prepare,同样,由资源对 XA 协议的支持来保证 持久化
-
-
完成阶段:
-
分支提交:执行 XA 分支的 commit
-
分支回滚:执行 XA 分支的 rollback
-
在执行阶段,会一直保持数据库连接对象,直到二阶段之后才会释放本地连接对象
AT和XA模式数据源代理机制对比
XA模式的使用
从编程模型上,XA 模式与 AT 模式保持完全一致。只需要修改数据源代理,即可实现 XA 模式与 AT 模式之间的切换
@Bean("dataSource")
public DataSource dataSource(DruidDataSource druidDataSource) {
// DataSourceProxy for AT mode
// return new DataSourceProxy(druidDataSource);
// DataSourceProxyXA for XA mode
return new DataSourceProxyXA(druidDataSource);
}
微服务整合SeataXA
Spring Cloud Alibaba整合Seata XA实战
对比Seata AT模式配置,只需修改两个地方:
- 微服务数据库不需要undo_log表,undo_log表仅用于AT模式
- 修改数据源代码模式为XA模式
seata:
# 数据源代理模式 默认AT
data-source-proxy-mode: XA
完整配置如下
seata:
# 是否开启spring-boot自动装配,默认true,包括数据源的自动代理以及GlobalTransactionScanner初始化
enabled: true
# 数据源代理模式 默认AT
data-source-proxy-mode: XA
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: nacos.mall.com:8848
namespace:
group: SEATA_GROUP
username: nacos
password: nacos
config:
# 指定nacos作为配置中心
type: nacos
nacos:
server-addr: nacos.mall.com:8848
namespace: 7e838c12-8554-4231-82d5-6d93573ddf32
group: SEATA_GROUP
data-id: seataServer.properties
username: nacos
password: nacos
如果使用SeataXA模式,在全局事务开启方,它自己本方法也要操作数据的的话,那么该方法处了要使用@GlobalTransactional
注解之外还要使用@Transactional
@Override
@GlobalTransactional(name="createOrder",rollbackFor=Exception.class)
@Transactional
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 ? "成功" : "失败");
// OpenFeign 扣减库存
storageFeignService.deduct(orderVo.getCommodityCode(), orderVo.getCount());
// OpenFeign 扣减余额
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;
}
SeataTCC模式
什么是TCC
TCC 基于分布式事务中的二阶段提交协议实现,它的全称为 Try-Confirm-Cancel,即资源预留(Try)、确认操作(Confirm)、取消操作(Cancel),他们的具体含义如下:
- Try:对业务资源的检查并预留;
- Confirm:对业务处理进行提交,即 commit 操作,只要 Try 成功,那么该步骤一定成功;
- Cancel:对业务处理进行取消,即回滚操作,该步骤回对 Try 预留的资源进行释放。
- XA是资源层面的分布式事务,强一致性,在两阶段提交的整个过程中,一直会持有资源的锁。
- TCC是业务层面的分布式事务,最终一致性,不会一直持有资源的锁。
TCC 是一种侵入式的分布式事务解决方案,以上三个操作都需要业务系统自行实现,对业务系统有着非常大的入侵性,设计相对复杂,但优点是 TCC 完全不依赖数据库,能够实现跨数据库、跨应用资源管理,对这些不同数据访问通过侵入式的编码方式实现一个原子操作,更好地解决了在各种复杂业务场景下的分布式事务问题。
常见开源TCC框架:
-
Seata TCC
-
Hmily
-
Tcc-Transaction
-
ByteTCC
-
EasyTransaction
以用户下单为例
在TCC模式下我们一般对数值型字段采用冻结,对非数值型添加status状态字段。比如我在库存量字段旁加一个冻结字段,在try阶段我扣减库存字段的值,在冻结字段位置进行增加,confirm阶段就直接处理冻结字段的数值,cancel阶段就减冻结字段的值 加库存字段的值
在金融领域,转账业务下,我们一般是在confirm阶段才会去为对方账号进行余额相加的操作。避免出现try阶段把钱转过去了,对方去使用钱,结果最后转账放要进行cancel,对方账号的钱缺已经使用了。
try-commit
try 阶段首先进行预留资源,然后在 commit 阶段扣除资源。如下图:
try-cancel
try 阶段首先进行预留资源,预留资源时扣减库存失败导致全局事务回滚,在 cancel 阶段释放资源。如下图:
Seata TCC 模式
一个分布式的全局事务,整体是 两阶段提交 的模型。
全局事务是由若干分支事务组成的,分支事务要满足 两阶段提交 的模型要求,即需要每个分支事务都具备自己的:
- 一阶段 prepare 行为
- 二阶段 commit 或 rollback 行为
在Seata中,AT模式与TCC模式事实上都是两阶段提交的具体实现,他们的区别在于:
AT 模式基于 支持本地 ACID 事务的关系型数据库:
- 一阶段 prepare 行为:在本地事务中,一并提交业务数据更新和相应回滚日志记录。
- 二阶段 commit 行为:马上成功结束,自动异步批量清理回滚日志。
- 二阶段 rollback 行为:通过回滚日志,自动生成补偿操作,完成数据回滚。
相应的,TCC 模式不依赖于底层数据资源的事务支持:
- 一阶段 prepare 行为:调用自定义的 prepare 逻辑。
- 二阶段 commit 行为:调用自定义的 commit 逻辑。
- 二阶段 rollback 行为:调用自定义的 rollback 逻辑。
简单点概括,Seata的TCC模式就是手工的AT模式,它允许你自定义两阶段的处理逻辑而不依赖AT模式的undo_log表。
Seata TCC模式接口改造
假设现有一个业务需要同时使用服务 A 和服务 B 完成一个事务操作,我们在服务 A 定义该服务的一个 TCC 接口:
public interface TccActionOne {
@TwoPhaseBusinessAction(name = "prepare", commitMethod = "commit", rollbackMethod = "rollback")
public boolean prepare(BusinessActionContext actionContext, @BusinessActionContextParameter(paramName = "a") String a);
public boolean commit(BusinessActionContext actionContext);
public boolean rollback(BusinessActionContext actionContext);
}
同样,在服务 B 定义该服务的一个 TCC 接口:
public interface TccActionTwo {
@TwoPhaseBusinessAction(name = "prepare", commitMethod = "commit", rollbackMethod = "rollback")
public void prepare(BusinessActionContext actionContext, @BusinessActionContextParameter(paramName = "b") String b);
public void commit(BusinessActionContext actionContext);
public void rollback(BusinessActionContext actionContext);
}
在业务所在系统中开启全局事务并执行服务 A 和服务 B 的 TCC 预留资源方法:
@GlobalTransactional
public String doTransactionCommit(){
//服务A事务参与者
tccActionOne.prepare(null,"one");
//服务B事务参与者
tccActionTwo.prepare(null,"two");
}
以上就是使用 Seata TCC 模式实现一个全局事务的例子,TCC 模式同样使用 @GlobalTransactional 注解开启全局事务,而服务 A 和服务 B 的 TCC 接口为事务参与者,Seata 会把一个 TCC 接口当成一个 Resource,也叫 TCC Resource。
TCC如何控制异常
在 TCC 模型执行的过程中,还可能会出现各种异常,其中最为常见的有空回滚、幂等、悬挂等。TCC 模式是分布式事务中非常重要的事务模式,但是幂等、悬挂和空回滚一直是 TCC 模式需要考虑的问题,Seata 框架在 1.5.1 版本完美解决了这些问题。
空回滚
空回滚指的是在一个分布式事务中,在没有调用参与方的 Try 方法的情况下,TM 驱动二阶段回滚调用了参与方的 Cancel 方法。
空回滚产生原因分析
全局事务开启,参与者A向TC注册了分支事务会立刻执行参与者一阶段RPC方法,如果此时参与者A机器宕机或网络异常,导致RPC方法调用失败 未执行。但此时全局事务已经开启,TC最终会全局事务回滚,调用参与者A进行回滚Cannel方法,从而造成了空回滚
要想防止空回滚,那么必须在 Cancel 方法中识别这是一个空回滚,Seata 是如何做的呢?
Seata 的做法是新增一个 TCC 事务控制表,包含事务的 XID 和 BranchID 信息,在 Try 方法执行时插入一条记录,表示一阶段执行了,执行 Cancel 方法时读取这条记录,如果记录不存在,说明 Try 方法没有执行。
幂等
幂等问题指的是 TC 重复进行二阶段提交,因此 Confirm/Cancel 接口需要支持幂等处理,即不会产生资源重复提交或者重复释放。
那么幂等问题是如何产生的呢
参与者 A 执行完二阶段之后,由于网络抖动或者宕机问题,会造成 TC 收不到参与者 A 执行二阶段的返回结果,TC 会重复发起调用,直到二阶段执行结果成功。
Seata 是如何处理幂等问题的呢?
同样的也是在 TCC 事务控制表中增加一个记录状态的字段 status,该字段有 3 个值,分别为:
- tried:1
- committed:2
- rollbacked:3
二阶段 Confirm/Cancel 方法执行后,将状态改为 committed 或 rollbacked 状态。当重复调用二阶段 Confirm/Cancel 方法时,判断事务状态即可解决幂等问题。
悬挂
悬挂指的是二阶段 Cancel 方法比 一阶段 Try 方法优先执行,由于允许空回滚的原因,在执行完二阶段 Cancel 方法之后直接空回滚返回成功,此时全局事务已结束,但是由于 Try 方法随后执行,这就会造成一阶段 Try 方法预留的资源永远无法提交和释放了。
那么悬挂是如何产生的呢?
如上图所示,在执行参与者 A 的一阶段 Try 方法时,出现网路拥堵,由于 Seata 全局事务有超时限制,执行 Try 方法超时后,TM 决议全局回滚,回滚完成后如果此时 RPC 请求才到达参与者 A,执行 Try 方法进行资源预留,从而造成悬挂。
Seata 是怎么处理悬挂的呢?
在 TCC 事务控制表记录状态的字段 status 中增加一个状态:
- suspended:4
当执行二阶段 Cancel 方法时,如果发现 TCC 事务控制表没有相关记录,说明二阶段 Cancel 方法优先一阶段 Try 方法执行,因此插入一条 status=4 状态的记录,当一阶段 Try 方法后面执行时,判断 status=4 ,则说明有二阶段 Cancel 已执行,并返回 false 以阻止一阶段 Try 方法执行成功。
微服务整合SeataTCC
用户下单,整个业务逻辑由三个微服务构成:
- 库存服务:对给定的商品扣除库存数量。
- 订单服务:根据采购需求创建订单。
- 帐户服务:从用户帐户中扣除余额。
环境准备
父pom指定微服务版本
Spring Cloud Alibaba Version | Spring Cloud Version | Spring Boot Version | Seata Version |
---|---|---|---|
2022.0.0.0 | 2022.0.0 | 3.0.2 | 1.7.0 |
-
启动Seata Server(TC)端,Seata Server使用nacos作为配置中心和注册中心
-
启动nacos服务
微服务导入seata依赖
spring-cloud-starter-alibaba-seata内部集成了seata,并实现了xid传递
<!-- seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
微服务application.yml中添加seata配置
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: 127.0.0.1:8848
namespace:
group: SEATA_GROUP
username: nacos
password: nacos
config:
# 指定nacos作为配置中心
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: 7e838c12-8554-4231-82d5-6d93573ddf32
group: SEATA_GROUP
data-id: seataServer.properties
username: nacos
password: nacos
注意:请确保client与server的注册中心和配置中心namespace和group一致
定义TCC接口
TCC相关注解如下:
@LocalTCC
适用于SpringCloud+Feign模式下的TCC,@LocalTCC
一定需要注解在接口上,此接口可以是寻常的业务接口,只要实现了TCC的两阶段提交对应方法便可@TwoPhaseBusinessAction
注解try方法,其中name
为当前tcc方法的bean名称,写方法名便可(全局唯一),commitMethod
指向提交方法,rollbackMethod
指向事务回滚方法。指定好三个方法之后,seata会根据全局事务的成功或失败,去帮我们自动调用提交方法或者回滚方法。@BusinessActionContextParameter
注解可以将参数传递到二阶段(commitMethod/rollbackMethod)的方法。BusinessActionContext
便是指TCC事务上下文
订单Order接口,首先自己先生成一条订单信息,调用库存storage服务扣减库存、调用用户account服务进行扣款账户余额。
首先的是订单的接口:
- 在try阶段会插入一条订单记录,为原始的订单表添加一个status代表状态的字段
- 在commit阶段,将status的状态修改为success状态
- 在rollback阶段,将status的状态改为fail状态
// 全局事务 发起者
public interface BussinessService {
/**
* 保存订单
*/
Order saveOrder(OrderVo orderVo) ;
}
@Service
@Slf4j
public class BusinessServiceImpl implements BussinessService {
@Autowired
private AccountFeignService accountFeignService;
@Autowired
private StorageFeignService storageFeignService;
// OrderService 就是我们接下来要定义的
@Autowired
private OrderService orderService;
// 全局事务发起者,使用@GlobalTransaction注解
@Override
@GlobalTransactional(name="createOrder",rollbackFor=Exception.class)
public Order saveOrder(OrderVo orderVo) {
log.info("=============用户下单=================");
log.info("当前 XID: {}", RootContext.getXID());
//获取全局唯一订单号 测试使用
Long orderId = UUIDGenerator.generateUUID();
//阶段一: 创建订单
Order order = orderService.prepareSaveOrder(orderVo,orderId);
//扣减库存
storageFeignService.deduct(orderVo.getCommodityCode(), orderVo.getCount());
//扣减余额
accountFeignService.debit(orderVo.getUserId(), orderVo.getMoney());
return order;
}
}
/**
* 通过 @LocalTCC 这个注解,RM 初始化的时候会向 TC 注册一个分支事务。
*/
@LocalTCC
public interface OrderService {
/**
* TCC的try方法:保存订单信息,状态为支付中
*
* 定义两阶段提交,在try阶段通过@TwoPhaseBusinessAction注解定义了分支事务的 resourceId,commit和 cancel 方法
* name = 该tcc的bean名称,全局唯一
* commitMethod = commit 为二阶段确认方法
* rollbackMethod = rollback 为二阶段取消方法
* BusinessActionContextParameter注解 传递参数到二阶段中,二阶段方法中通过paramName属性指定的key获取到值
* useTCCFence seata1.5.1的新特性,用于解决TCC幂等,悬挂,空回滚问题,需在服务端数据库中增加日志表tcc_fence_log
*/
@TwoPhaseBusinessAction(name = "prepareSaveOrder", commitMethod = "commit", rollbackMethod = "rollback", useTCCFence = true)
Order prepareSaveOrder(OrderVo orderVo, @BusinessActionContextParameter(paramName = "orderId") Long orderId);
/**
*
* TCC的confirm方法:订单状态改为支付成功
*
* 二阶段确认方法可以另命名,但要保证与commitMethod一致
* context可以传递try方法的参数
*
* @param actionContext
* @return
*/
boolean commit(BusinessActionContext actionContext);
/**
* TCC的cancel方法:订单状态改为支付失败
* 二阶段取消方法可以另命名,但要保证与rollbackMethod一致
*
* @param actionContext
* @return
*/
boolean rollback(BusinessActionContext actionContext);
}
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper orderMapper;
@Override
@Transactional(rollbackFor=Exception.class)
public Order prepareSaveOrder(OrderVo orderVo,@BusinessActionContextParameter(paramName = "orderId") Long orderId) {
// 保存订单
Order order = new Order();
order.setId(orderId);
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 ? "成功" : "失败");
return order;
}
@Override
public boolean commit(BusinessActionContext actionContext) {
// 获取订单id
// 因为接口的方法中使用了@BusinessActionContextParameter(paramName = "orderId"),所以这里就能从actionContext中获取
long orderId = Long.parseLong(actionContext.getActionContext("orderId").toString());
//更新订单状态为支付成功
Integer updateOrderRecord = orderMapper.updateOrderStatus(orderId,OrderStatus.SUCCESS.getValue());
log.info("更新订单id:{} {}", orderId, updateOrderRecord > 0 ? "成功" : "失败");
return true;
}
// TCC模式 进行手动补偿机制
@Override
public boolean rollback(BusinessActionContext actionContext) {
//获取订单id
long orderId = Long.parseLong(actionContext.getActionContext("orderId").toString());
//更新订单状态为支付失败
Integer updateOrderRecord = orderMapper.updateOrderStatus(orderId,OrderStatus.FAIL.getValue());
log.info("更新订单id:{} {}", orderId, updateOrderRecord > 0 ? "成功" : "失败");
return true;
}
}
库存storage服务:
- 在原库存表中新增一个冻结库存的字段,先检查库存是否充足
- try方法中,库存字段中的值 -count 而冻结库存的值+count
- 在commit方法中,直接操作冻结库存字段的值,冻结存储的值-count
- 在rollback方法中,需要库存字段中的值 +count 而冻结库存的值-count
/**
* 通过 @LocalTCC 这个注解,RM 初始化的时候会向 TC 注册一个分支事务。
*/
@LocalTCC
public interface StorageService {
/**
* Try: 库存-扣减数量,冻结库存+扣减数量
*
* 定义两阶段提交,在try阶段通过@TwoPhaseBusinessAction注解定义了分支事务的 resourceId,commit和 cancel 方法
* name = 该tcc的bean名称,全局唯一
* commitMethod = commit 为二阶段确认方法
* rollbackMethod = rollback 为二阶段取消方法
* BusinessActionContextParameter注解 传递参数到二阶段中
*
* @param commodityCode 商品编号
* @param count 扣减数量
* @return
*/
@TwoPhaseBusinessAction(name = "deduct", commitMethod = "commit", rollbackMethod = "rollback", useTCCFence = true)
boolean deduct(@BusinessActionContextParameter(paramName = "commodityCode") String commodityCode,
@BusinessActionContextParameter(paramName = "count") int count);
/**
*
* Confirm: 冻结库存-扣减数量
* 二阶段确认方法可以另命名,但要保证与commitMethod一致
* context可以传递try方法的参数
*
* @param actionContext
* @return
*/
boolean commit(BusinessActionContext actionContext);
/**
* Cancel: 库存+扣减数量,冻结库存-扣减数量
* 二阶段取消方法可以另命名,但要保证与rollbackMethod一致
*
* @param actionContext
* @return
*/
boolean rollback(BusinessActionContext actionContext);
}
@Service
@Slf4j
public class StorageServiceImpl implements StorageService {
@Autowired
private StorageMapper storageMapper;
@Transactional
@Override
public boolean deduct(String commodityCode, int count){
log.info("=============冻结库存=================");
log.info("当前 XID: {}", RootContext.getXID());
// 检查库存
checkStock(commodityCode,count);
log.info("开始冻结 {} 库存", commodityCode);
//冻结库存
Integer record = storageMapper.freezeStorage(commodityCode,count);
log.info("冻结 {} 库存结果:{}", commodityCode, record > 0 ? "操作成功" : "扣减库存失败");
return true;
}
@Override
public boolean commit(BusinessActionContext actionContext) {
log.info("=============扣减冻结库存=================");
String commodityCode = actionContext.getActionContext("commodityCode").toString();
int count = (int) actionContext.getActionContext("count");
//扣减冻结库存
storageMapper.reduceFreezeStorage(commodityCode,count);
return true;
}
@Override
public boolean rollback(BusinessActionContext actionContext) {
log.info("=============解冻库存=================");
String commodityCode = actionContext.getActionContext("commodityCode").toString();
int count = (int) actionContext.getActionContext("count");
//扣减冻结库存
storageMapper.unfreezeStorage(commodityCode,count);
return true;
}
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("库存不足");
}
}
}
用户Account服务:
- 在原有的余额字段之外,新增一个冻结余额字段。检查账户余额是否充足
- try阶段,余额字段的值 -money ,冻结余额字段的值 +money
- commit阶段,仅操作冻结余额字段,冻结余额字段的值 -money
- rollback阶段,余额字段的值+money ,冻结余额字段的值 -money
/**
* 通过 @LocalTCC 这个注解,RM 初始化的时候会向 TC 注册一个分支事务。
*/
@LocalTCC
public interface AccountService {
/**
* 用户账户扣款
*
* 定义两阶段提交,在try阶段通过@TwoPhaseBusinessAction注解定义了分支事务的 resourceId,commit和 cancel 方法
* name = 该tcc的bean名称,全局唯一
* commitMethod = commit 为二阶段确认方法
* rollbackMethod = rollback 为二阶段取消方法
*
* @param userId
* @param money 从用户账户中扣除的金额
* @return
*/
@TwoPhaseBusinessAction(name = "debit", commitMethod = "commit", rollbackMethod = "rollback", useTCCFence = true)
boolean debit(@BusinessActionContextParameter(paramName = "userId") String userId,
@BusinessActionContextParameter(paramName = "money") int money);
/**
* 提交事务,二阶段确认方法可以另命名,但要保证与commitMethod一致
* context可以传递try方法的参数
*
* @param actionContext
* @return
*/
boolean commit(BusinessActionContext actionContext);
/**
* 回滚事务,二阶段取消方法可以另命名,但要保证与rollbackMethod一致
*
* @param actionContext
* @return
*/
boolean rollback(BusinessActionContext actionContext);
}
@Service
@Slf4j
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountMapper accountMapper;
/**
* 扣减用户金额
* @param userId
* @param money
*/
@Transactional
@Override
public boolean debit(String userId, int money){
log.info("=============冻结用户账户余额=================");
log.info("当前 XID: {}", RootContext.getXID());
// 检查余额
checkBalance(userId, money);
log.info("开始冻结用户 {} 余额", userId);
//冻结金额
Integer record = accountMapper.freezeBalance(userId,money);
log.info("冻结用户 {} 余额结果:{}", userId, record > 0 ? "操作成功" : "扣减余额失败");
return true;
}
@Override
public boolean commit(BusinessActionContext actionContext) {
log.info("=============扣减冻结金额=================");
String userId = actionContext.getActionContext("userId").toString();
int money = (int) actionContext.getActionContext("money");
//扣减冻结金额
accountMapper.reduceFreezeBalance(userId,money);
return true;
}
@Override
public boolean rollback(BusinessActionContext actionContext) {
log.info("=============解冻金额=================");
String userId = actionContext.getActionContext("userId").toString();
int money = (int) actionContext.getActionContext("money");
//解冻金额
accountMapper.unfreezeBalance(userId,money);
return true;
}
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("余额不足");
}
}
}
微服务增加tcc_fence_log日志表
# tcc_fence_log 建表语句如下(MySQL 语法)
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;
比较
一阶段连接对象操作 | rollback补偿机制 | 说明 | |
---|---|---|---|
AT | 查询操作、业务sql执行、查询操作、往undo_log表插入数据、提交事务释放连接对象 | 自动补偿,通过after image进行补偿 | 需要依赖于ACID的关系型数据库;不适用于对于性能要求很高、高并发的场景 |
XA | 进行业务sql执行,预提交,一直持有连接对象 | 自动补偿,连接对象直接rollback | 需要依赖于ACID、XA的关系型数据库;因为是强一致性,也不适用于性能要求高的场景 |
TCC | 在try阶段 执行业务sql、记录日志表、提交事务释放连接对象 | 手动补偿,在cancel方法中自定义业务补偿;需要注意空回滚/幂等/悬挂。 | 不依赖于数据库;相比较AT模式更适用于性能要求高的场景 |