Spring 事务处理失效

Posted by Pismery Liu on Sunday, July 12, 2020

TOC

本文重点介绍 Spring 事务处理的几个失效的场景和相关解决方案;

Spring 事务处理失效

在日常业务开发过程中,事务处理在保证数据一致性起到了重要作用;而 Java 中常用 Spring 的声明式事务控制事务;即 Spring 注解 @Transaction;

Spring 的声明式事务依旧是 JDBC 的事务 API 上的封装实现;其通过 AOP 的方式对指定方法动态增强,为方法添加事务的功能;默认的 AOP 实现方式是动态代理;动态代理的实现方式有 JDK 代理和 CGLIB 代理,Spring 会根据指定条件判断使用哪种动态代理实现;大致实现如下:

public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {

    @Override
    public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
        if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
            Class<?> targetClass = config.getTargetClass();
            if (targetClass == null) {
                throw new AopConfigException("TargetSource cannot determine target class: " +
                        "Either an interface or a target is required for proxy creation.");
            }
            if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
                return new JdkDynamicAopProxy(config);
            }
            return new ObjenesisCglibAopProxy(config);
        }
        else {
            return new JdkDynamicAopProxy(config);
        }
    }

    /**
     * Determine whether the supplied {@link AdvisedSupport} has only the
     * {@link org.springframework.aop.SpringProxy} interface specified
     * (or no proxy interfaces specified at all).
     */
    private boolean hasNoUserSuppliedProxyInterfaces(AdvisedSupport config) {
        Class<?>[] ifcs = config.getProxiedInterfaces();
        return (ifcs.length == 0 || (ifcs.length == 1 && SpringProxy.class.isAssignableFrom(ifcs[0])));
    }

}

对于 AOP 方面有机会再另行讨论;我们回到主题,先总结一下,导致 Spring 事务处理异常的情况:

  • 没有通过 Spring Bean 的方式调用事务方法;
  • 在不使用 AspectJ 实现代理情况下,对非 public 方法进行 @Transaction 申明;
  • 异常被 catch 没有抛出;
  • 抛出的异常为 checked exception「受检异常」;
  • 数据库本身不支持事务,如 MySQL 的 MyISAM 存储引擎;

下面逐一分析以上几种事务处理异常的情况;

没有通过 Spring Bean 的方式调用事务方法;

首先,先看看错误示例

@Service
public class PersonService {

    public void updatePerson() {
        doUpdatePerson();
    }

    @Transaction
    public void doUpdatePerson() {
        ....
    }
}

我们知道 Spring 是通过动态代理的方式实现声明式事务的;因此,要想对方法实现事务控制,必须通过 Spring 增强后的代理对象来调用方法;类内部方法自调用没有经过 Spring Bean 动态增强,最终方法 doUpdatePerson 没有事务控制;

解决方式也很简单,下面展示三种方式:

方式一

通过 Autowired 自身,再用 Autowied 进来的 Bean 来调用相应方法即可;

@Service
public class PersonService {
    @Autowired
    private PersonService self;

    public void updatePerson() {
        self.doUpdatePerson();
    }

    @Transaction
    public void doUpdatePerson() {
        ....
    }
}

方式二

将方法移动到另一个类中,再 Autowird 新的类,来调用

@Service
public class PersonService {
    @Autowired
    private PersonDoService doService;

    public void updatePerson() {
        doService.doUpdatePerson();
    }
}

@Service
public class PersonDoService {
    @Transaction
    public void doUpdatePerson() {
        ....
    }
}

方式三

通过 ApplicationContext 的 getBean 方法获取增强代理对象;

@Service
public class PersonService {
    @Autowired
    private ApplicationContext ctx;

    public void updatePerson() {
        PersonService self = (PersonService) ctx.getBean("personService");
        self.doUpdatePerson();
    }

    @Transaction
    public void doUpdatePerson() {
        ....
    }
}

日常开发过程中,建议使用方式二;方式一的实现方式,一方面对于不了解 Spring 事务的同事,可能会十分疑惑这样的设计;另一方面其还可能引入循环依赖的问题;

下面我们看看会导致循环依赖问题的示例:

@Service
public class PersonService {
    @Autowired
    private PersonService self;

    public void updatePerson() {
        self.doUpdatePerson();
    }

    @Transaction
    @Aync
    public void doUpdatePerson() {
        ....
    }
}

示例很简单,只是在原来的基础上加上了注解 @Async,来实现方法异步的增强;此时启动 Spring 容器将会启动异常,错误信息如下:

org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'personService': 
Bean with name 'personService' has been injected into other beans [personService] in its raw version as part of a circular reference, 
but has eventually been wrapped. This means that said other beans do not use the final version of the bean. 
This is often the result of over-eager type matching - consider using 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.

错误信息意思是,初始化 personService 时,此对象已经注入 personService 注入,形成了循环依赖;解决方式也很简单,通过 @Lazy 注解实现延迟加载即可;实现如下:

@Service
public class PersonService {
    @Autowired
    @Lazy
    private PersonService self;

    public void updatePerson() {
        self.doUpdatePerson();
    }

    @Transaction
    @Aync
    public void doUpdatePerson() {
        ....
    }
}

在不使用 AspectJ 实现代理情况下,对非 public 方法进行 @Transaction 申明

错误示例如下:

@Service
public class PersonService {
    @Autowired
    private PersonService self;

    public void updatePerson() {
        self.doUpdatePerson1();
        self.doUpdatePerson2();
        self.doUpdatePerson3();
    }

    @Transaction
    protected void doUpdatePerson1() {
        ....
    }

    @Transaction
    default void doUpdatePerson2() {
        ....
    }

    @Transaction
    private void doUpdatePerson3() {
        ....
    }
}

解决方式:将事务方法设置为 public 方法;

异常被 catch 没有抛出

方法出现异常,本希望事务进行回滚操作,但是以下代码却发现,事务未回滚,正常提交;

@Service
public class PersonService {
    @Transaction
    public void updatePerson() {
        try {
            doUpdatePerson();
        } catch(Exception e) {
            log(e);
        }
    }

    private void doUpdatePerson() {
        ....
        throw new RunTimeExcpetion();
    }

}

Spring 处理事务逻辑如下:

try {
   // This is an around advice: Invoke the next interceptor in the chain.
   // This will normally result in a target object being invoked.
   retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
   // target invocation exception
   completeTransactionAfterThrowing(txInfo, ex);
   throw ex;
}
finally {
   cleanupTransactionInfo(txInfo);
}

只有当 Spring 捕获到了异常,才能够执行 completeTransactionAfterThrowing 方法进行事务回滚;因此,当我们方法将异常捕获后,没有抛出则不会进行事务回滚;

解决方案如下:

@Service
public class PersonService {
    @Transaction
    public void updatePerson() {
        try {
            doUpdatePerson();
        } catch(Exception e) {
            log(e);
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }
    }

    private void doUpdatePerson() {
        ....
        throw new RunTimeExcpetion();
    }

}

通过 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly() 显示设置 RollbackOnly,使得事务进行回滚

抛出的异常为 checked exception「受检异常」

有时候我们抛出了异常,但是事务还是没有回滚,下面我们看看示例:

@Service
public class PersonService {
    @Transaction
    public void updatePerson() throws IOException{
        doUpdatePerson();
    }

    private void doUpdatePerson() throws IOException{
        ....
        readFile();
    }

}

当读取文件时,会抛出一个受检异常 IOException;但是 Spring 默认情况下,出现 RuntimeException(非受检异常)或 Error 的时候,Spring 才会回滚事务。

Spring 官方解释是受检异常一般是业务异常或者是另一种方法返回值,出现这样的异常,业务可能还能完成,因此不会主动回滚;而 Error 或 RuntimeException 代表了非预期的结果,应该回滚;

解决方案:

方式一

如果希望自己处理异常,但仍要回滚事务;

@Service
public class PersonService {
    @Transaction
    public void updatePerson(){
        try {
            doUpdatePerson();
        } catch(Exception e) {
            log(e);
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }
    }

    private void doUpdatePerson() throws IOException{
        ....
        readFile();
    }

}

方式二

使用 rollbackFor 指定需要回滚的异常;

@Service
public class PersonService {
    @Transaction(rollbackFor = Exception.class)
    public void updatePerson() throws IOException{
        doUpdatePerson();
    }

    private void doUpdatePerson() throws IOException{
        ....
        readFile();
    }

}

数据库本身不支持事务,如 MySQL 的 MyISAM 存储引擎

这种情况解决方案就更换 MySQL 存储引擎为 InnoDB;

参考资料

  • 「极客时间」Java业务开发常见错误100例 之 06 | 20%的业务代码的Spring声明式事务,可能都没处理正确
  • 「极客时间」每日一课 之 内部方法调用时,为什么Spring AOP增强不生效?