乐趣区

关于springboot:Spring事务失效场景

  1. 抛出查看异样
    比方你的事务控制代码如下:

@Transactional
public void transactionTest() throws IOException{

User user = new User();
UserService.insert(user);
throw new IOException();

}
复制代码
如果 @Transactional 没有特地指定,Spring 只会在遇到运行时异样 RuntimeException 或者 error 时进行回滚,而 IOException 等查看异样不会影响回滚。

public boolean rollbackOn(Throwable ex) {

return (ex instanceof RuntimeException || ex instanceof Error);

}
复制代码
解决方案:

晓得起因后,解决办法也很简略。配置 rollbackFor 属性,例如 @Transactional(rollbackFor = Exception.class)。

  1. 业务办法自身捕捉了异样
    @Transactional(rollbackFor = Exception.class)
    public void transactionTest() {
    try {

     User user = new User();
     UserService.insert(user);
     int i = 1 / 0;

    }catch (Exception e) {

     e.printStackTrace();

    }
    }
    复制代码
    这种场景下,事务失败的起因也很简略,Spring 是否进行回滚是依据你是否抛出异样决定的,所以如果你本人捕捉了异样,Spring 也无能为力。

看了下面的代码,你可能认为这么简略的问题你不可能犯这么愚昧的谬误,然而我想通知你的是,我身边简直一半的人都被这一幕困扰过。

写业务代码的时候,代码可能比较复杂,嵌套的办法很多。如果你不小心,很可能会触发此问题。举一个非常简单的例子,假如你有一个审计性能。每个办法执行后,审计后果保留在数据库中,那么代码可能会这样写。

@Service
public class TransactionService {

@Transactional(rollbackFor = Exception.class)
public void transactionTest() throws IOException {User user = new User();
    UserService.insert(user);
    throw new IOException();}

}

@Component
public class AuditAspect {

@Autowired
private auditService auditService;

@Around(value = "execution (* com.alvin.*.*(..))")
public Object around(ProceedingJoinPoint pjp) {
    try {Audit audit = new Audit();
        Signature signature = pjp.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        String[] strings = methodSignature.getParameterNames();
        audit.setMethod(signature.getName());
        audit.setParameters(strings);
        Object proceed = pjp.proceed();
        audit.success(true);
        return proceed;
    } catch (Throwable e) {log.error("{}", e);
        audit.success(false);
    }
    
    auditService.save(audit);
    return null;
}

}
复制代码
在下面的示例中,事务将失败。起因是 Spring 的事务切面优先级最低,所以如果异样被切面捕捉,Spring 天然不能失常处理事务,因为事务管理器无奈捕捉异样。

解决方案:

看,尽管咱们晓得在处理事务时业务代码不能自己捕捉异样,然而只有代码变得复杂,咱们就很可能再次出错,所以咱们在处理事务的时候要小心,还是不要应用申明式事务, 并应用编程式事务— transactionTemplate.execute()。

  1. 同一类中的办法调用
    @Service
    public class DefaultTransactionService implement Service {

    public void saveUser() throws Exception {

     //do something
     doInsert();

    }

    @Transactional(rollbackFor = Exception.class)
    public void doInsert() throws IOException {

     User user = new User();
     UserService.insert(user);
     throw new IOException();
    

    }
    }
    复制代码
    这也是一个容易出错的场景。事务失败的起因也很简略,因为 Spring 的事务管理性能是通过动静代理实现的,而 Spring 默认应用 JDK 动静代理,而 JDK 动静代理采纳接口实现的形式,通过反射调用指标类。简略了解,就是 saveUser()办法中调用 this.doInsert(), 这里的 this 是被实在对象,所以会间接走 doInsert 的业务逻辑,而不会走切面逻辑,所以事务失败。

解决方案:

计划一:解决办法能够是间接在启动类中增加 @Transactional 注解 saveUser()

计划二:@EnableAspectJAutoProxy(exposeProxy = true)在启动类中增加,会由 Cglib 代理实现。

  1. 办法应用 final 或 static 关键字
    如果 Spring 应用了 Cglib 代理实现(比方你的代理类没有实现接口),而你的业务办法恰好应用了 final 或者 static 关键字,那么事务也会失败。更具体地说,它应该抛出异样,因为 Cglib 应用字节码加强技术生成被代理类的子类并重写被代理类的办法来实现代理。如果被代理的办法的办法应用 final 或 static 关键字,则子类不能重写被代理的办法。

如果 Spring 应用 JDK 动静代理实现,JDK 动静代理是基于接口实现的,那么 final 和 static 润饰的办法也就无奈被代理。

总而言之,办法连代理都没有,那么必定无奈实现事务回滚了。

解决方案:

想方法去掉 final 或者 static 关键字

  1. 办法不是 public
    如果办法不是 public,Spring 事务也会失败,因为 Spring 的事务管理源码 AbstractFallbackTransactionAttributeSource 中有判断 computeTransactionAttribute()。如果指标办法不是公共的,则 TransactionAttribute 返回 null。

// Don’t allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
复制代码
解决方案:

是将以后办法拜访级别更改为 public。

  1. 谬误应用流传机制
    Spring 事务的流传机制是指在多个事务办法互相调用时,确定事务应该如何流传的策略。Spring 提供了七种事务流传机制:REQUIRED、SUPPORTS、MANDATORY、REQUIRES_NEW、NOT_SUPPORTED、NEVER、NESTED。如果不晓得这些流传策略的原理,很可能会导致交易失败。

@Service
public class TransactionService {

@Autowired
private UserMapper userMapper;

@Autowired
private AddressMapper addressMapper;


@Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)
public  void doInsert(User user,Address address) throws Exception {
    //do something
    userMapper.insert(user);
    saveAddress(address);
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public  void saveAddress(Address address) {
    //do something
    addressMapper.insert(address);
}

}
复制代码
在下面的例子中,如果用户插入失败,不会导致 saveAddress()回滚,因为这里应用的流传是 REQUIRES_NEW,流传机制 REQUIRES_NEW 的原理是如果以后办法中没有事务,就会创立一个新的事务。如果一个事务曾经存在,则以后事务将被挂起,并创立一个新事务。在以后事务实现之前,不会提交父事务。如果父事务产生异样,则不影响子事务的提交。

事务的流传机制阐明如下:

REQUIRED 如果以后上下文中存在事务,那么退出该事务,如果不存在事务,创立一个事务,这是默认的流传属性值。
SUPPORTS 如果以后上下文存在事务,则反对事务退出事务,如果不存在事务,则应用非事务的形式执行。
MANDATORY 如果以后上下文中存在事务,否则抛出异样。
REQUIRES_NEW 每次都会新建一个事务,并且同时将上下文中的事务挂起,执行以后新建事务实现当前,上下文事务复原再执行。
NOT_SUPPORTED 如果以后上下文中存在事务,则挂起以后事务,而后新的办法在没有事务的环境中执行。
NEVER 如果以后上下文中存在事务,则抛出异样,否则在无事务环境上执行代码。
NESTED 如果以后上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务。
解决方案:

将事务流传策略更改为默认值 REQUIRED。REQUIRED 原理是如果以后有一个事务被增加到一个事务中,如果没有,则创立一个新的事务,父事务和被调用的事务在同一个事务中。即便被调用的异样被捕捉,整个事务依然会被回滚。

  1. 没有被 Spring 治理
    // @Service
    public class OrderServiceImpl implements OrderService {
    @Transactional
    public void updateOrder(Order order) {

     // update order

    }
    }
    复制代码
    如果此时把 @Service 注解正文掉,这个类就不会被加载成一个 Bean,那这个类就不会被 Spring 治理了,事务天然就生效了。

解决方案:

须要保障每个事务注解的每个 Bean 被 Spring 治理。

  1. 多线程
    @Service
    public class UserService {

    @Autowired
    private UserMapper userMapper;
    @Autowired
    private RoleService roleService;

    @Transactional
    public void add(UserModel userModel) throws Exception {

     userMapper.insertUser(userModel);
     new Thread(() -> {
          try {test();
          } catch (Exception e) {roleService.doOtherThing();
          }
     }).start();

    }
    }

@Service
public class RoleService {

@Transactional
public void doOtherThing() {
     try {
         int i = 1/0;
         System.out.println("保留 role 表数据");
     }catch (Exception e) {throw new RuntimeException();
    }
}

}

退出移动版