Spring事务失效和处理方式|Java开发实战

这是我参与更文挑战的第1天,活动详情查看: 更文挑战

本文正在参加「Java主题月 - Java 开发实战」,详情查看 活动链接

一、前言

事务(Transaction),一般是指要做的或所做的事情。在计算机术语中是指访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。AOP利用的是动态代理,AOP中有个接口是 方法拦截器,借助这个接口我们可以在想要操作的方法外加一些操作。事务拦截的对象是TransactionInterceptor,可以看出它继承了TransactionAspectSupport. TransactionAspectSupport内部是真正的操作部分。

事务的关键对象

Spring事务中有几个对象很重要,理解了这几个对象就相当于抓住了总体,剩下的一些细节多花些时间就懂了。

  • PlatformTransactionManager 事务管理器,听名字就知道它是管理事务的操作的,它只包含三个方法。获取事务,回顾事务,提交事务。

  • TransactionDefiition 定义事务的类型,事务包含很多属性,是否可读,事务隔离级别,事务传播级别。通过事务的定义,我们根据定义获取特定的事务。

  • TransactionStatus 代表一个事务运行的状态,事务管理器通过状态可以知道事务的状态信息,然后进行事务的控制。事务是否完成,是否是新的事务,是不是只能回滚等。

事务传播级别的处理

事务传播级别处理是事务中的一个重点,那么源码中如何处理的呢?从创建事务部分开始看

  • 处理事务属性标识,标识大家都理解就是这个谁的属性
  • 如果属性不为空,并且事务管理器不为空那么,获取事务tm.getTransaction.
  • 最后准备TransactionInfo
protected TransactionInfo createTransactionIfNecessary(
    PlatformTransactionManager tm, TransactionAttribute txAttr, final String joinpointIdentification) {

    // If no name specified, apply method identification as transaction name.
    if (txAttr != null && txAttr.getName() == null) {
        txAttr = new DelegatingTransactionAttribute(txAttr) {
            @Override
            public String getName() {
                return joinpointIdentification;
            }
        };
    }

    TransactionStatus status = null;
    if (txAttr != null) {
        if (tm != null) {
            status = tm.getTransaction(txAttr);
        }
        else {
            if (logger.isDebugEnabled()) {
                logger.debug("Skipping transactional joinpoint [" + joinpointIdentification +
                        "] because no transaction manager has been configured");
            }
        }
    }
    return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
}
复制代码

tm.getTransaction,如何获取事务的呢?

  • doGetTransaction是抽象方法,由其父类实现,看名字知道这个是真正获取事务对象的方法,单数据源的一般实现是DatasourceTransactionManager.

  • 创建事务的定义,TransactionDefinition,前面有说到这个对象的内容,定义事务的类型等

  • 如果已经存在了事务,根据事务的传播级别进行存在事务处理;
    如果不存在事务,根据定义设置事务的超时时间,是否只读,是否新建事务

    在使用Spring的事务管理时,有时候就会莫名其妙的发现事务没生效,其实并非Spring事务管理本身的问题,而是开发人员在使用时使用方式不对,Spring事务管理的底层机制没搞懂,又或者数据库层的问题导致,今天总结下Spring事务是失效的集中场景。

二、常见的Spring事务失效原因

① 没有被Spring管理

不是由Spring管理的Bean,例如,一个类没有在xml中声明或者没有@Service,@RestController,@Component等注解的方式声明由Spring管理,但在类的方法中使用@Transactional注解声明事务,这种情况事务是不会生效的,也就是说Spring管理的事务只能是在Spring容器管理的Bean中声明才会生效。如下代码,testA方法虽然声明了事务,但是由于@Service注解被注释了,所以事务不会生效。
复制代码
//@Service
public class TransactionService {
    private final AccountMapper accountMapper;
    private Account account = null;

    @Autowired
    public TransactionService(AccountMapper accountMapper) {
        this.accountMapper = accountMapper;
    }

    @Transactional(propagation = Propagation.NESTED)
    public void testA() {
        insertB();
        account.getBalance();
        insertB();
    }

    public void insertA() {
        Account account = new Account();
        account.setBalance(new Long(100));
        account.setCreateTime(new Date());
        account.setFreezeAmount(new Long(0));
        account.setUpdateTime(new Date());
        account.setUserId("123");
        accountMapper.insertSelective(account);
    }

    public void insertB() {
        Account account = new Account();
        account.setBalance(new Long(200));
        account.setCreateTime(new Date());
        account.setFreezeAmount(new Long(0));
        account.setUpdateTime(new Date());
        account.setUserId("456");
        accountMapper.insertSelective(account);
    }
}
复制代码

② 发生自调用

先上代码,required方法没有加注解声明事务,required方法调用了testA方法,testA方法声明了事务,但是这里事务不会生效,其原因是因为Spring事务管理是通过代理类实现的,然而这种自调用相当于this.testA(),是通过对象调用,无法走到事务的切面中,所以事务就不生效了。

@Service
public class TransactionService {
    private final AccountMapper accountMapper;
    private Account account = null;

    @Autowired
    public TransactionService(AccountMapper accountMapper) {
        this.accountMapper = accountMapper;
    }
    
    public void required() {
        insertA();
        testA();
    }

    @Transactional
    public void testA() {
        insertB();
        account.getBalance();
        insertB();
    }
}
复制代码

再看下面代码,这时required()方法声明了事务,这时testA方法声明了REQUIRES_NEW传播机制的事务,testA方法的事务会生效吗?答案是不会生效,一样的问题产生了自调用,并没有经过Spring的代理,所以事务不生效。

@Service
public class TransactionService {
    private final AccountMapper accountMapper;
    private Account account = null;

    @Autowired
    public TransactionService(AccountMapper accountMapper) {
        this.accountMapper = accountMapper;
    }
    
    @Transactional
    public void required() {
        insertA();
        testA();
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void testA() {
        insertB();
        account.getBalance();
        insertB();
    }
}
复制代码

③ 方法不是public的

来自 Spring 官方文档:

When using proxies, you should apply the @Transactional annotation only to methods with public visibility. If you do annotate protected, private or package-visible methods with the @Transactional annotation, no error is raised, but the annotated method does not exhibit the configured transactional settings. Consider the use of AspectJ (see below) if you need to annotate non-public methods.

意思是 @Transactional 只能用于 public 的方法上,否则事务不会失效,如果要用在非 public 方法上,可以开启 AspectJ 代理模式。

④ 数据源未配置事务管理器

数据源没有配置事务管理器,声明了事务也是白搭,下面是配置事务管理器的代码

@Bean(name = "transactionManager")
public DataSourceTransactionManager sentinelTransactionManager(@Qualifier("datasource") DataSource dataSource) {
    return new DataSourceTransactionManager(dataSource);
}
复制代码

⑤ 数据库引擎不支持事务

以MySQL为例,其MyISAM引擎是不支持事务操作的,InnoDB才是支持事务的引擎,一般要支持事务都会使用InnoDB。如果数据库引擎不支持事务,那也是白搭。

⑥ 事务传播机制设置以不支持事务运行

Propagation.NOT_SUPPORTED: 表示不以事务运行,当前若存在事务则挂起。

⑦ 异常被catch掉了

如下面的代码,如果insertB方法中,插入数据库后产生异常,并不会回滚,因为异常被catch调了,事务失效。

@Transactional
public void testA() {
    try {
        insertB();
    } catch (Exception e) {
        e.printStackTrace();
    }
}
复制代码

⑧ 异常类型错误

事务默认回滚的异常是RuntimeException,如果想触发其他异常的回滚,需要在注解上配置,例如:

@Transactional(rollbackFor = Exception.class)
复制代码

三、事务失效处理方式

常见的事务失效原因:

  • 如使用mysql且引擎是MyISAM,则事务会不起作用,原因是MyISAM不支持事务,可以改成InnoDB。

  • 如果使用了spring+mvc,则context:component-scan重复扫描问题可能会引起事务失败。

  • @Transactional 注解开启配置,必须放到listener里加载,如果放到DispatcherServlet的配置里,事务也是不起作用的。 

  • @Transactional 注解只能应用到 public 可见度的方法上。 如果你在 protected、private 或者 package-visible 的方法上使用 @Transactional 注解,它也不会报错,事务也会失效。 

  • Spring团队建议在具体的类(或类的方法)上使用 @Transactional 注解,而不要使用在类所要实现的任何接口上。在接口上使用 @Transactional 注解,只能当你设置了基于接口的代理时它才生效。因为注解是 不能继承 的,这就意味着如果正在使用基于类的代理时,那么事务的设置将不能被基于类的代理所识别,而且对象也将不会被事务代理所包装。

四、总结

本文总结了八种事务失效的场景,其实发生最多就是自身调用、异常被吃、异常抛出类型不对这三个了。而遇到事务失效的时候,先看一下是否被事务管理器接管了,如果有,那么多半是容器的问题,这里并不一定是父子容器,也有可能是父父容器。