背景

微服务架构商城项目中有一段关于支付功能的业务,逻辑为:

  1. 修改订单状态,涉及到订单微服务;
  2. 扣减用户余额,涉及到用户微服务;
  3. 修改支付单状态,涉及到支付微服务。

分布式事务代码如下,由于这些业务逻辑分布在不同的服务模块之下,修改订单状态和扣减余额通过Open Feign实现,同时使用Sentinel进行服务熔断,在扣减余额的Client编写了服务降级逻辑。

服务降级代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Slf4j
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {
@Override
public UserClient create(Throwable cause) {
return new UserClient() {
@Override
public void deductMoney(String pw, Integer amount) {
log.info("Fallback-扣减余额失败!");
throw new RuntimeException(cause);
}
};
}
}

支付业务代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@GlobalTransactional	// 回滚成功
// @GlobalTransactional(rollbackFor = Error.class) // 回滚成功
// @GlobalTransactional(rollbackFor = Exception.class) // 回滚失败,照常提交
// @GlobalTransactional(rollbackFor = Throwable.class) // 回滚成功
public void tryPayOrderByBalance(PayOrderFormDTO payOrderFormDTO) {
// 1.查询支付单
PayOrder po = getById(payOrderFormDTO.getId());
// 2.判断状态
if(!PayStatus.WAIT_BUYER_PAY.equalsValue(po.getStatus())){
// 订单不是未支付,状态异常
throw new BizIllegalException("交易已支付或关闭!");
}
// 3.修改订单状态
tradeClient.markOrderPaySuccess(po.getBizOrderNo());
// 4.尝试扣减余额
userClient.deductMoney(payOrderFormDTO.getPw(), po.getAmount());
// 5.修改支付单状态
boolean success = markPayOrderSuccess(payOrderFormDTO.getId(), LocalDateTime.now());
if (!success) {
throw new BizIllegalException("交易已支付或关闭!");
}
}

问题

当扣减余额失败时,需要发生回滚:

  • 使用@GlobalTransactional(rollbackfor = Exception.class)回滚失效,事务直接提交了;
    image-20250128192211835
  • 使用@GlobalTransactional、@GlobalTransactional(rollbackFor = Throwable.class)或@GlobalTransactional(rollbackFor = Error.class),均回滚成功;
    image-20250128181502737

原因

问题出在 Sentinel 的 SentinelInvocationHandler 类中,对于 fallback 结果的后续处理如下:

image-20250128195213336

Sentinel 会将 Feign Client 抛出的异常进一步封装为 AssertionError,然后抛出。因此,在调用者中无法通过 Exception 触发回滚,这就是为什么 rollbackFor = Exception 时无法正常回滚。

其他三种情况可以通过 Error 触发回滚,因此正常(默认情况下,rollbackFor 未指定时,未检查异常和 Error 会触发回滚;Throwable 是整个异常体系的顶层接口,包含了 ExceptionError)。

进一步验证该结论,将 rollbackFor 修改为 AssertionError.class,可以正常回滚:

image-20250128200325164

__END__