• 事务失效

    事务失效我们一般要从两个方面排查问题

    数据库层面

    数据库层面,数据库使用的存储引擎是否支持事务?默认情况下MySQL数据库使用的是Innodb存储引擎(5.5版本之后),它是支持事务的,但是如果你的表特地修改了存储引擎,例如,你通过下面的语句修改了表使用的存储引擎为MyISAM,而MyISAM又是不支持事务的

    alter table table_name engine=myisam;

    这样就会出现“事务失效”的问题了

    「解决方案」:修改存储引擎为Innodb

    业务代码层面

    业务层面的代码是否有问题,这就有很多种可能了

    「解决方案」:将Bean交由Spring进行管理(添加@Service注解)

    也就是说,默认情况下你无法使用@Transactional对一个非public的方法进行事务管理

    「解决方案」:修改需要事务管理的方法为public

    @Service
    public class DmzService {
     
     public void saveAB(A a, B b) {
      saveA(a);
      saveB(b);
     }

     @Transactional
     public void saveA(A a) {
      dao.saveA(a);
     }
     
     @Transactional
     public void saveB(B b){
      dao.saveB(a);
     }
    }

    上面三个方法都在同一个类DmzService中,其中saveAB方法中调用了本类中的saveAsaveB方法,这就是自调用。在上面的例子中saveAsaveB上的事务会失效

    那么自调用为什么会导致事务失效呢?我们知道Spring中事务的实现是依赖于AOP的,当容器在创建dmzService这个Bean时,发现这个类中存在了被@Transactional标注的方法(修饰符为public)那么就需要为这个类创建一个代理对象并放入到容器中,创建的代理对象等价于下面这个类

    public class DmzServiceProxy {

        private DmzService dmzService;

        public DmzServiceProxy(DmzService dmzService) {
            this.dmzService = dmzService;
        }

        public void saveAB(A a, B b) {
            dmzService.saveAB(a, b);
        }

        public void saveA(A a) {
            try {
                // 开启事务
                startTransaction();
                dmzService.saveA(a);
            } catch (Exception e) {
                // 出现异常回滚事务
                rollbackTransaction();
            }
            // 提交事务
            commitTransaction();
        }

        public void saveB(B b) {
            try {
                // 开启事务
                startTransaction();
                dmzService.saveB(b);
            } catch (Exception e) {
                // 出现异常回滚事务
                rollbackTransaction();
            }
            // 提交事务
            commitTransaction();
        }
    }

    上面是一段伪代码,通过startTransactionrollbackTransactioncommitTransaction这三个方法模拟代理类实现的逻辑。因为目标类DmzService中的saveAsaveB方法上存在@Transactional注解,所以会对这两个方法进行拦截并嵌入事务管理的逻辑,同时saveAB方法上没有@Transactional,相当于代理类直接调用了目标类中的方法。

    我们会发现当通过代理类调用saveAB时整个方法的调用链如下:

    一个@Transactional哪里来这么多坑?-LMLPHP

    实际上我们在调用saveAsaveB时调用的是目标类中的方法,这种清空下,事务当然会失效。

    常见的自调用导致的事务失效还有一个例子,如下:

    @Service
    public class DmzService {
     @Transactional
     public void save(A a, B b) {
      saveB(b);
     }
     
     @Transactional(propagation = Propagation.REQUIRES_NEW)
     public void saveB(B b){
      dao.saveB(a);
     }
    }

    当我们调用save方法时,我们预期的执行流程是这样的

    一个@Transactional哪里来这么多坑?-LMLPHP

    也就是说两个事务之间互不干扰,每个事务都有自己的开启、回滚、提交操作。

    但根据之前的分析我们知道,实际上在调用saveB方法时,是直接调用的目标类中的saveB方法,在saveB方法前后并不会有事务的开启或者提交、回滚等操作,实际的流程是下面这样的

    一个@Transactional哪里来这么多坑?-LMLPHP

    由于saveB方法实际上是由dmzService也就是目标类自己调用的,所以在saveB方法的前后并不会执行事务的相关操作。这也是自调用带来问题的根本原因:「自调用时,调用的是目标类中的方法而不是代理类中的方法」

    「解决方案」

    这里我们做个来做个小总结

    总结

    一图胜千言

    事务回滚相关问题

    回滚相关的问题可以被总结为两句话

    先看第一种情况:「想回滚的时候事务却提交了」。这种情况往往是程序员对Spring中事务的rollbackFor属性不够了解导致的。

    对应代码其实我们上篇文章也分析过了,如下:

    默认情况下,只有出现RuntimeException或者Error才会回滚

    public boolean rollbackOn(Throwable ex) {
        return (ex instanceof RuntimeException || ex instanceof Error);
    }

    所以,如果你想在出现了非RuntimeException或者Error时也回滚,请指定回滚时的异常,例如:

    @Transactional(rollbackFor = Exception.class)

    第二种情况:「想提交的时候被标记成只能回滚了(rollback only)」

    对应的异常信息如下:

    Transaction rolled back because it has been marked as rollback-only

    我们先来看个例子吧

    @Service
    public class DmzService {

     @Autowired
     IndexService indexService;

     @Transactional
     public void testRollbackOnly() {
      try {
       indexService.a();
      } catch (ClassNotFoundException e) {
       System.out.println("catch");
      }
     }
    }

    @Service
    public class IndexService {
     @Transactional(rollbackFor = Exception.class)
     public void a() throws ClassNotFoundException{
      // ......
      throw new ClassNotFoundException();
     }
    }

    在上面这个例子中,DmzServicetestRollbackOnly方法跟IndexServicea方法都开启了事务,并且事务的传播级别为required,所以当我们在testRollbackOnly中调用IndexServicea方法时这两个方法应当是共用的一个事务。按照这种思路,虽然IndexServicea方法抛出了异常,但是我们在testRollbackOnly将异常捕获了,那么这个事务应该是可以正常提交的,为什么会抛出异常呢?

    如果你看过我之前的源码分析的文章应该知道,在处理回滚时有这么一段代码

    在提交时又做了下面这个判断(这个方法我删掉了一些不重要的代码

    可以看到当提交时发现事务已经被标记为rollbackOnly后会进入回滚处理中,并且unexpected传入的为true。在处理回滚时又有下面这段代码

    最后在这里抛出了这个异常。

    总结起来,「主要的原因就是因为内部事务回滚时将整个大事务做了一个rollbackOnly的标记」,所以即使我们在外部事务中catch了抛出的异常,整个事务仍然无法正常提交,并且如果你希望正常提交,Spring还会抛出一个异常。

    「解决方案」:

    这个解决方案要依赖业务而定,你要明确你想要的结果是什么

    虽然这两者都能得到上面的结果,但是它们之间还是有不同的。当传播级别为requires_new时,两个事务完全没有联系,各自都有自己的事务管理机制(开启事务、关闭事务、回滚事务)。但是传播级别为nested时,实际上只存在一个事务,只是在调用a方法时设置了一个保存点,当a方法回滚时,实际上是回滚到保存点上,并且当外部事务提交时,内部事务才会提交,外部事务如果回滚,内部事务会跟着回滚。

    通过显示的设置事务的状态为RollbackOnly。这样当提交事务时会进入下面这段代码

    最大的区别在于处理回滚时第二个参数传入的是false,这意味着回滚是回滚是预期之中的,所以在处理完回滚后并不会抛出异常。

    读写分离跟事务结合使用时的问题

    读写分离一般有两种实现方式

    如果是配置了多数据源的方式实现了读写分离,那么需要注意的是:「如果开启了一个读写事务,那么必须使用写节点」「如果是一个只读事务,那么可以使用读节点」

    如果是依赖于MyCat等中间件那么需要注意:「只要开启了事务,事务内的SQL都会使用写节点(依赖于具体中间件的实现,也有可能会允许使用读节点,具体策略需要自行跟DB团队确认)」

    基于上面的结论,我们在使用事务时应该更加谨慎,在没有必要开启事务时尽量不要开启。

    其次,关于如果没有对只读事务做优化的话(优化意味着将只读事务路由到读节点),那么@Transactional注解中的readOnly属性就应该要慎用。我们使用readOnly的原本目的是为了将事务标记为只读,这样当MySQL服务端检测到是一个只读事务后就可以做优化,少分配一些资源(例如:只读事务不需要回滚,所以不需要分配undo log段)。但是当配置了读写分离后,可能会可能会导致只读事务内所有的SQL都被路由到了主库,读写分离也就失去了意义。

    总结

    这篇文章主要是总结了工作中事务相关的常见问题,想让大家少走点弯路!希望大家可以认真读完哦!

    后记

    一个@Transactional哪里来这么多坑?-LMLPHP

    文章有帮助可以点个「在看」或「分享」,都是支持,我都喜欢!

    我是Guide哥,Java后端开发,会一点前端知识,喜欢烹饪,自由的少年。一个三观比主角还正的技术人。我们下期再见!

    本文分享自微信公众号 - JavaGuide(JavaGuide)。
    如有侵权,请联系 [email protected] 删除。
    本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

    09-09 21:40