浅谈spring事务失效之谜

引言:

每当我们使用Spring声明式事务时,我们只需要在类或方法上声明@Transactional就可以了,但是我们有没有遇到事务失效的时候呢?

spring事务失效七种大的原因

  1. 如使用mysql且引擎是MyISAM,则事务会不起作用,原因是MyISAM不支持事务,可以改成InnoDB。
  2. 如果使用了spring+mvc,则context:component-scan重复扫描问题可能会引起事务失败。
  3. @Transactional 注解开启配置,必须放到listener里加载,如果放到DispatcherServlet的配置里,事务也是不起作用的。
  4. @Transactional 注解只能应用到 public 可见度的方法上。 如果你在 protected、private 或者 的方法上使用 @Transactional 注解,它也不会报错,事务也会失效。
  5. Spring团队建议在具体的类(或类的方法)上使用 @Transactional 注解,而不要使用在类所要实现的任何接口上。在接口上使用 @Transactional 注解,只能当你设置了基于接口的代理时它才生效。因为注解是 不能继承 的,这就意味着如果正在使用基于类的代理时,那么事务的设置将不能被基于类的代理所识别,而且对象也将不会被事务代理所包装(但是放在接口上面也是可以实现事务滴,我还没搞清)。
  6. 一个事务方法调用同一个类里面的另一个事务方法,被调用的方法的事务失效。(使用了原生对象,使得代理失效)
  7. 将异常代码使用throw new Exception(“xxxxxxxxxxxx”)进行捕获

Spring的父子容器(放到DispatcherServlet配置事务失效)

父容器:Root WebAplicationContext
子容器:Servlet WebAplicationContext
在这里插入图片描述
如图,可以看到子容器有什么需要的是可以向父容器要的,但是父容器是没法向子容器要的。所以当我们是使用Spring+SpringMVC进行web应用开发的时候,Spring负责扫描DAO和Service层,SpringMVC负责扫描controller就行了(请不要在扫描DAO和Service层,自己做自己的事情就可以了,虽然这样做不会出错,但是浪费了内存空间)。

这张图也解释了为什么我们在SpringMVC中配置了事务扫描却不生效------》一般做事务处理都是在Service层,我们把事务扫描配置在SpringMVC中,我们的父容器是无法得到的,这样就导致了事务失效。。。。。

所以必须放到listener里加载,而不是webMVC的配置里

一个事务方法调用同一个类里面的另一个事务方法,被调用的方法的事务失效

(根本原因–》JDK代理或Cglib代理失效,使用了原生对象)

@Service
public class CityServiceImpl implements CityService {
    private final static Logger LOGGER = LoggerFactory.getLogger(CityServiceImpl.class);
    
    @Autowired
     private CityDao cityDao;
    
     private CityServiceImpl proxy;

    @Override
    @Transactional
    public void parent() {
        LOGGER.info("======================insertParent()===================");
        //这种情况child事务失效
        //根据动态代理分析 此处的child()不是由 AopProxy调用的 而是 this对象
        try {                  
            child();                        
            }catch (Exception e){
            LOGGER.error("parent catch child execption ",e);
            throw new  RuntimeException();
        }
        //以下代码为Parent业务
        City city= new City();
        city.setProvinceId(Long.valueOf(99));
        city.setCityName("parent");
        city.setDescription("parentparentparentparent");
        cityDao.insertCity(city);
    }
  
    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void child() {
        LOGGER.info("======================insertChild()===================");
        City city= new City();
        city.setProvinceId(Long.valueOf(99));
        city.setCityName("Child");
        city.setDescription("ChildChildChildChildChildChild");
        cityDao.insertCity(city);
        int a=1/0;//此处异常
    }
    

我们运行这段代码以后会发现,当Child发生异常时,插入的数据却并没有回滚,这时就发生了事务失效。 接下来我们通过我上面给出的Spring事务失效的原因来分析,假设我们并没有犯了前五条的错误,那么这时候我们的问题来了,到底是什么造成了Child方法事务失效呢?

我们知道SpringAop和事务的实现都是通过JDK或Cglib动态代理来实现的,那么会不会是他们失效了呢?

失效的真正原因

Spring事务失效的原因 -----》简单刨析

目前Spring实现动态代理的方式有两种,一种是cglib,一种是jdk的,两个的实现方式不一样,但是事务失效原因是一样的。接下来我们以JDK动态代理为例

JDKProxy

public interface JDKProxy {

    void parent();

    void child();
}

JDKProxyImpl

public class JDKProxyImpl implements  JDKProxy{

    public void parent() {
        System.out.println("parent......");
//        child();
    }

    @Override
    public void child() {
        System.out.println("child......");
    }
}

JDKProxyTest

package fileTest;/*
 @author yyc
 @DESCRIPTION 
 @create 2019/8/19
*/

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class JDKProxyTest {

    public static void main(String[] args) {
    
       JDKProxy jdkProxy = new JDKProxyImpl();
       ProxyInvocationHandler handler = new ProxyInvocationHandler(jdkProxy);
       JDKProxy jdkProxy1 = (JDKProxy) Proxy.newProxyInstance(jdkProxy.getClass().getClassLoader(), jdkProxy.getClass().getInterfaces(), handler);
       
       jdkProxy1.parent();
       jdkProxy1.child();
       System.out.println(jdkProxy1.getClass().getName());
    }
}

class ProxyInvocationHandler implements InvocationHandler{

    private JDKProxy target;

    public ProxyInvocationHandler(JDKProxy target) {
        this.target=target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("代理方法被执行了。。。");
        return method.invoke(target, args);
    }

}

运行以后,我们发现,两个方法都被增强了
在这里插入图片描述
然后我们在JDKProxyImpl中的parent方法中使用child方法,同时在JDKProxyTest中只运行parent方法,此时我们理所当然的认为执行结果应该还会上次一样。

假设
在这里插入图片描述

现实
在这里插入图片描述
这不就是和我们上述Spring失效的场景是一样的么?而我们的Spring事务实现不就是依赖JDK或Cglib动态代理实现的么,所以此场景下Spring失效的原因就是因为底层JDK或Cglib动态代理失效。

当然到这,我们还是没有分析清除Spring事务是怎么失效的(JDK代理为何会在此处失效),下面让我们来继续瞎掰

JDK代理失效的原因 ----》深入刨析

在这里插入图片描述
我们知道child()和this.child()是等价的,那么在这的this是谁呢,通过打印在这里插入图片描述
我们可以看到此处的this是fileTest.JDKProxyImpl

而我们被增强的Parent方法,又是被谁调用而增强的呢,
在这里插入图片描述
在这里插入图片描述
之前,直接在main方法中运行 jdkProxy1.parent(); jdkProxy1.child();都被增强,也是使用了代理对象。

好了,现在我们终于知道了,为什么parent方法调用同一个类的Child方法会使Child的事务失效。----》想要使用JDK动态代理,就必须使用代理对象。所以想要让Spring事务生效,也就必须使用代理对象。

那么我们该如何解决上面这个问题呢?

  1. 最简单的办法就是不要写在一个类里面。。。。
  2. 从当前线程的AopContext中获取
CityServiceImpl proxy=(CityServiceImpl)AopContext.currentProxy();
proxy.child()
  1. 通过spring应用上下文 获取 代理对象(ApplicationContext 在ioc容器中是单例的)
 @Autowired
    private ApplicationContext context;
    @PostConstruct
    public  void init(){
        proxy=context.getBean(CityServiceImpl.class);
    }
 proxy.child()

事务方法发生异常却不回滚

Spring的Transactional的API文档:

If no rules are relevant to the exception, it will be treated like DefaultTransactionAttribute (rolling back on runtime exceptions).

在业务代码中,有如下两种情况,比如:
throw new RuntimeException(“xxxxxxxxxxxx”); 事务回滚
throw new Exception(“xxxxxxxxxxxx”); 事务没有回滚
spring内部catch的就是 RuntimeException, service抛出RuntimeException可以回滚
如果抛出Exception,就不回滚….

1. Spring的AOP即声明式事务管理默认是针对unchecked exception回滚。

也就是默认对RuntimeException()异常或是其子类进行事务回滚;checked异常,即Exception可try{}捕获的不会回滚,如果使用try-catch捕获抛出的unchecked异常后没有在catch块中采用页面硬编码的方式使用spring api对事务做显式的回滚,则事务不会回滚, “将异常捕获,并且在catch块中不对事务做显式提交=生吞掉异常” ,要想捕获非运行时异常则需要如下配置:

解决办法:
1.在针对事务的类中抛出RuntimeException异常,而不是抛出Exception。
2.在txAdive中增加rollback-for,里面写自己的exception,例如自己写的exception:

<tx:advice id="txAdvice" transaction-manager="transactionManager">
   <tx:attributes>
     <tx:method name="*" rollback-for="com.cn.untils.exception.XyzException"/>
   </tx:attributes>
 </tx:advice>

或者定义不会滚的异常

<tx:advice id="txAdvice">
    <tx:attributes>
       <tx:method name="update*" no-rollback-for="IOException"/>
       <tx:method name="*"/>
    </tx:attributes>
 </tx:advice>

2. spring的事务边界是在调用业务方法之前开始的,业务方法执行完毕之后来执行commit or rollback(Spring默认取决于是否抛出runtime异常).

如果抛出runtime exception 并在你的业务方法中没有catch到的话,事务会回滚。
一般不需要在业务方法中catch异常,如果非要catch,在做完你想做的工作后(比如关闭文件等)一定要抛出runtime exception,否则spring会将你的操作commit,这样就会产生脏数据.所以你的catch代码是画蛇添足。

例如:

try {  
    //bisiness logic code  
} catch(Exception e) {  
    //handle the exception  
}  

由此可以推知,在spring中如果某个业务方法被一个 整个包裹起来,则这个业务方法也就等于脱离了spring事务的管理,因为没有任何异常会从业务方法中抛出!全被捕获并吞掉,导致spring异常抛出触发事务回滚策略失效。

注:不过,如果在catch代码块中采用页面硬编码的方式使用spring api对事务做显式的回滚,这样写也未尝不可。

3. 基于注解的事务:

Transactional的异常控制,默认是Check Exception 不回滚,unCheck Exception回滚
如果配置了rollbackFor 和 noRollbackFor 且两个都是用同样的异常,那么遇到该异常,还是回滚
rollbackFor 和noRollbackFor 配置也许不会含盖所有异常,对于遗漏的按照Check Exception 不回滚,unCheck Exception回滚

如果只是@Transactional失效的话,可以考虑改成:@Transactional(rollbackFor=Exception.class)
例子如下,在类或方法上的加入:

@Transactional(rollbackFor=Exception.class)
发布了45 篇原创文章 · 获赞 3 · 访问量 2314

猜你喜欢

转载自blog.csdn.net/weixin_44046437/article/details/99712344
今日推荐