1. 问题描述与重现
jfoa ( github.com/JavaFamilyC… ) 提供了 Audit 资源操作记录的功能, 目标方法(比如添加用户)存在事务管理, 在 Audit 中需要去查询用户(事务管理), 最后在目标方法执行后执行 Audit 记录的插入(事务管理). 即添加用户(事务)中查询用户(事务)就会抛出异常:
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
字面意思是: 出现了不可预知的回滚异常,因为事务已经被标志为只能回滚,所以事务回滚了.
下面来重现和描述一下这个问题.
@Around("audit()") public Object recordLog(ProceedingJoinPoint pjp) throws Throwable { Log log = null; Customer principal = null; try { // note1: 这里获取当前登录的用户, 注意这个方法可以抛出异常 // 1) getCurrentCustomer 在事务管理中 // 2) catch 了方法调用并没有继续抛出异常 principal = customerService.getCurrentCustomer(); } catch (Exception ignore) { LOGGER.debug("Get principal error!", ignore); } // ... 省略一些中间代码 Object result; try { // note3: 具体的目标方法 result = pjp.proceed(); } catch(Throwable throwable) { if(log != null) { log.setMessage("Execute Failed: " + throwable.getMessage()); } throw throwable; } finally { // ... } return result; }
// note2: 目标方法有事务管理 @Audit(ResourceEnum.Customer) @Transactional @Override public Integer insertCustomer(@AuditObject("getName()") Customer user) { if(isValid(user)) { user.setPassword( SecurityUtil.generatorPassword(user.getAccount(), user.getPassword())); return customerDao.insert(user); } return null; }
注意上方的 note1 和 note2, 在这种情况下就会出现本文的异常.
2. 原理剖析
因为 spring 的 @Transactional 注解默认的事务传播策略为 Propagation.REQUIRED, PROPAGATION_REQUIRED的意思是,当前有事务,则使用当前事务,当前无事务则创建事务。因此, 当 目标方法被调用时, 由于目标方法存在事务, 当目标方法被调用时会开启事务, 当执行到 note1 时, getCurrentCustomer 方法也 存在事务, 根据事务传播策略, spring 就会使用目标方法的事务, 但是 getCurrentCustomer 有 try-catch 处理, 并且 catch 没有继续抛出事务, 因此当 getCurrentCustomer 抛出异常时, spring 要执行 rollback 操作, 但是 catch 没有继续抛出异常, 还会继续调用到 note3 处继续执行, 当目标方法被调用完成后 由于是正常调用完毕, 因此又需要执行 commit, 所以就会引发该异常.
2.0 事务传播策略简介
spring 定义了如下传播策略:
public enum Propagation { /** * 当前有事务,则使用当前事务,当前无事务则创建事务 */ REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED), /** * 支持当前事务,如果不存在,以非事务方式执行。 * 注:对于事务同步的事务管理器,与根本没有事务略有不同, * 因为它定义了同步将应用的事务范围。 * 结果,相同的资源(JDBC连接、Hibernate会话等) * 将为整个指定范围共享。注意,这取决于 * 事务管理器的实际同步配置。 */ SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS), /** * 如果当前与事务则使用当前事务, 如果当前没事务则抛出异常. */ MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY), /** * 无论当前是否有事务都创建一个新的事务. * 如果当前有事务则挂起当前事务. */ REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW), /** * 以非事务方式运行, 如果当前有事务, 则挂起当前事务 */ NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED), /** * 以非事务方式运行, 如果当前有事务, 则抛出异常 */ NEVER(TransactionDefinition.PROPAGATION_NEVER), /** * 如果当前事务存在,则在嵌套事务中执行,否则行为类似{@code REQUIRED}. * 注意: 嵌套事务的实际创建只适用于特定的事务管理器。 * 这只适用于JDBC DataSourceTransactionManager。 * 一些JTA提供程序可能也支持嵌套事务。 */ NESTED(TransactionDefinition.PROPAGATION_NESTED);
2.1 默认传播策略
spring 的 @Transactional 注解默认的事务传播策略为 Propagation.REQUIRED
public @interface Transactional { // ... /** * The transaction propagation type. * <p>Defaults to {@link Propagation#REQUIRED}. * @see org.springframework.transaction.interceptor.TransactionAttribute#getPropagationBehavior() */ Propagation propagation() default Propagation.REQUIRED;
2.2 问题总结
当 methodA 调用 methodB, 并且两个方都有事务(事务嵌套), 且传播策略都为 Propagation.REQUIRED 时, 就会导致两个方法 共用一个事务,methodB将事务标志为回滚,methodA中commit这个事务, 然后就会出现事务已经被标志回滚(methodB标志的)的异常信息.
3. 解决方法
知道了问题出现的原因, 自然就知道了解决办法, 但是不同的业务场景解决方式可能不同, 大家需要结合自己的业务场景选择合适的解决方案
3.1 修改 try-catch
对内部方法 methodB 不要进行 try-catch 或者 catch 继续抛出异常.
3.2 修改事务传播策略
如果修改外部事务 methodA 的事务传播策略, 则 methodA 只能取消事务(一般不可用).
修改内部 methodB 的事务传播策略, 一般可改为 REQUIRES_NEW, NOT_SUPPORTED, NESTED.
NOT_SUPPORTED
都把内部事务转换为非事务运行.
REQUIRES_NEW
会创建一个新事务运行.
NESTED
在嵌套事务运行.
比如我这里的业务场景, 首先是需要事务管理, 因此我这里可以使用 REQUIRES_NEW, NESTED, 其次, audit 记录以及, getCurrentCustomer 无论何时都不应该影响核心业务逻辑的运行,因此, 最终我选择修改事务传播策略为 REQUIRES_NEW 来解决这个问题. 如果您的嵌套事务和主事务有关联关系, 那么您就应该选择 NESTED.
最后, 还需要注意一点: 同一个类中,事务嵌套以最外层的方法为准,嵌套的事务失效;不同类中嵌套的事务才会生效