前言
方法内如果存在多个第三方写接口调用的情况下,也就是存在分布式事务的情况下,慎用注解式事务。
案例
本文针对的场景在业务开发中很常见,也就是上面所描述的场景,我们的下单接口内除了本地事务还包括对订单中心和支付中心的调用,并且这些调用之间还有数据依赖,在当前和前端的交互模式下,完全不可能用MQ做异步,但是我们还是需要保证这个分布式事务。
伪代码如下
@Transactional
PayInfo createOrder(){
//创建订单
order = createOrder();
//分布式事务-同步订单到订单中心
orderCode= orderCenter.sync(order)
order.setOrderCode(order)
//分布式事务-通过订单创建支付单
payInfo = paymentCenter.createPayment(order)
//本地事务-订单落库
orderService.add(order)
return payInfo
}
上面这段代码存在的问题是,如果创建支付单调用失败或者本地订单落库失败,对订单中心的调用并不能回滚,因此用户还是能在App上看到这个单子(App查询统一走订单中心,因为每个业务系统有不同类型的订单),但是实际上在我们系统内这个单子因为事务回滚掉了。
@Transactional仅能回滚本地事务,并且因为它的方便,我得不到这个本地事务的执行状态,进而去回滚分布式事务。
解决方案是,弃用注解式事务,使用编程式事务。spring对应提供了TransactionTemplate类。
TransactionTemplate
配置
多加一个bean配置即可
<bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
<constructor-arg name="transactionManager" ref="transactionManager" />
</bean>
使用
transactionTemplate.execute(new TransactionCallback<Object>{
@Override
public Object doInTransaction(TransactionStatus status) {
//你的事务操作
}
})
和使用@Transactional的区别在于,你需要把你的事务操作扔进TransactionCallback的doInTransaction方法。
如果在doInTransaction内抛出异常就会回滚当前事务。需要注意的是,这个异常并不会被框架层代码吞掉,execute方法会继续抛出它,因此我们就能通过捕获异常来回滚我们的分布式事务了。
有没有感受到spring事务传播性的原理
我们项目中具体使用案例如下
final ObjectHolder<String> orderCodeHolder = new ObjectHolder<>(null);
try {
CreateOrderResponseDTO createOrderResponseDTO = transactionTemplate.execute(new TransactionCallback<CreateOrderResponseDTO>() {
@Override
public CreateOrderResponseDTO doInTransaction(TransactionStatus status) {
// 一些校验 以及构造订单
OrderWithWaresDTO orderWithWaresDTO = validateAndBuildOrder(request);
// 调用订单中心创建订单
String orderCode = orderCenterService.registerOrder(orderWithWaresDTO);
orderCodeHolder.set(orderCode);
orderWithWaresDTO.setOrderCode(orderCode);
// 调用支付中心创建支付单
CreatePaymentOrderResponse response = paymentCenterService.createPayment(orderWithWaresDTO);
//...业务操作
//落本地数据路
createLocalOrder(orderWithWaresDTO);
CreateOrderResponseDTO createOrderResponseDTO = new CreateOrderResponseDTO();
//...业务操作
return createOrderResponseDTO;
}
});
return Result.success(createOrderResponseDTO);
}catch (Exception ex){
log.error("下单失败,回滚订单中心",ex);
if(Objects.nonNull(orderCodeHolder.get())){
// TODO: 2019-06-05 这边也有可能是失败 考虑做成重试
orderCenterService.deleteOrder(orderCodeHolder.get());
}
return Result.fail("创建订单失败");
}
transactionTemplate的execute方法传入了TransactionCallback接口的匿名实现,因此我们外部参数要和TransactionCallback中的方法进行交互需要声明为final类型,而String类型一旦声明就不能修改了,因此这边通过Holder类包装了一下。
public class ObjectHolder<T> {
private T object;
public ObjectHolder(T object){
this.object = object;
}
public T get() {
return object;
}
public void set(T object) {
this.object = object;
}
}
最后
在不能使用MQ的情况下,编程式事务肯定是最常用的一种解决分布式事务的方式。没有太多框架以及中间件限制。