深入Spring事务管理
大部分情况下一个数据库事务是要么同时成功,要么同时失败的,但是也存在的不同的要求,例如信用卡还款,有个跑批量的事务,而整个批量事务包含了对多个信用卡的还款业务的处理,我们不能因为一张卡的事务失败了,就把其他卡的事务也回滚,造成多个客户还款失败,即正常还款的用户,也被认为是不正常还款的,这样会引发很严重的金融信誉问题,Spring事务的传播行为带来了比较方便的解决方案。
@Override @Nullable public <T> T execute(TransactionCallback<T> action) throws TransactionException { Assert.state(this.transactionManager != null, "No PlatformTransactionManager set"); //使用自定义的事务管理器 if (this.transactionManager instanceof CallbackPreferringPlatformTransactionManager) { return ((CallbackPreferringPlatformTransactionManager) this.transactionManager).execute(this, action); }else {//系统默认管理器 //获取事务状态 TransactionStatus status = this.transactionManager.getTransaction(this); T result; try { result = action.doInTransaction(status);//回调接口方法 }catch (RuntimeException | Error ex) { // 回滚异常方法 rollbackOnException(status, ex); throw ex; }catch (Throwable ex) { // 回滚异常方法 rollbackOnException(status, ex); throw new UndeclaredThrowableException(ex, "TransactionCallback threw undeclared checked exception"); } this.transactionManager.commit(status);//提交事务 return result;//返回结果 } }
- 事务的创建、提交和回滚是通过PlatformTransactionManager接口来完成的
- 当事务产生异常时会回滚事务,在默认实现中所有的异常都会回滚。我们可以通过配置去修改在某些异常发生时回滚或者不回滚事务
- 当无异常时,提交事务
配置事务管理器
目前我们讨论JDBC和MyBatis,使用最多的事务管理器是DataSourceTransactionManager,其它持久层框架后续介绍。
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/tx https://www.springframework.org/schema/tx/spring-tx.xsd"> <context:component-scan base-package="com.wise.tiger"/> <context:property-placeholder location="classpath:dbcp-config.properties"/> <!-- 配置数据源 --> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close" > <property name="driverClassName" value="${driverClassName}" /> <property name="url" value="${url}" /> <property name="username" value="${jdbc.username}" /> <property name="password" value="${password}" /> <property name="defaultAutoCommit" value="${defaultAutoCommit}"/> <property name="connectionProperties" value="${connectionProperties}"/> </bean> <!--配置数据源事务管理器 --> <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> </beans>
声明式事务
声明式事务是一种约定性的事务,在大部分的情况下,使用数据库事务时,大部分场景是在代码中发生了异常时,需要回滚事务,而不发生异常时则是提交事务,从而保证数据库数据的一致性。从这点出发,Spring给了一个约定(类似于AOP开发给的约定),你的业务方法不发生异常,事务管理器就提交事务,发生异常则让事务管理器回滚事务。
首先声明式事务允许自定义事务接口——TransactionDefinition,它可以由xml或者注解@Transactional进行配置,到了这里我们先谈谈@Transactional的配置项
@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface Transactional { @AliasFor("transactionManager") String value() default ""; @AliasFor("value") String transactionManager() default ""; Propagation propagation() default Propagation.REQUIRED; Isolation isolation() default Isolation.DEFAULT; int timeout() default TransactionDefinition.TIMEOUT_DEFAULT; boolean readOnly() default false; Class<? extends Throwable>[] rollbackFor() default {}; String[] rollbackForClassName() default {}; Class<? extends Throwable>[] noRollbackFor() default {}; String[] noRollbackForClassName() default {}; }
配置项 | 含义 | 备注 |
value | 定义事务管理器 |
spring容器中的一个Bean id, 这个Bean需要实现接口PlatformTransactionManager |
transactionManager | 同上 | 同上 |
isolation | 隔离级别 | 数据库在并发事务时的概念,默认取值为数据库的隔离级别 |
propagation | 传播行为 | 方法之间调用问题,默认取值为Propagation.REQUIRED |
timeout | 超时时间 | 单位为秒,当超时时,会引发异常,默认会导致事务回滚 |
readOnly | 是否开启只读事务 | 默认值:false |
rollbackFor | 回滚事务的异常类定义 | 只有当方法产生所定义的异常时,才会回滚事务 |
rollbackForClassName | 回滚事务的异常类名定义 | 同rollbackFor,只是使用类名称定义 |
noRollbackFor | 当产生哪些异常不回滚事务 | 当产生所定义异常时,Spring将继续提交事务 |
noRollbackForClassName | 同noRollbackFor | 同noRollbackFor,只是使用类名称定义 |
只需要在xml配置文件中加入如下配置就可以使用@Transactional配置事务了
使用xml进行配置事务管理器
<!-- 配置事务通知 --> <tx:advice id="txAdvice" transaction-manager="txManager"> <tx:attributes> <!-- 根据方法名指定事务的属性 --> <tx:method name="bookService" propagation="REQUIRED"/> <tx:method name="get*" read-only="true"/> <tx:method name="find*" read-only="true"/> <tx:method name="*"/> </tx:attributes> </tx:advice> <!-- 配置事务切入点,以及把事务切入点和事务属性关联起来 --> <aop:config> <aop:pointcut expression="execution(* com.wise.tiger.service.*.*(..))" id="txPointcut"/> <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/> </aop:config>
数据库的相关知识(参考另一篇博文:https://adan-chiu.iteye.com/blog/2441253)
选择隔离级别和传播行为
选择隔离级别的出发点在于两点:性能和数据一致性
选择隔离级别
在互联网应用中,不但要考虑数据库数据的一致性,而且还要考虑系统的性能,一般而言,从脏读到序列化,系统性能直线下降。因此设置高的级别,比如序列化,会严重压制并发,从而引发大量的线程挂起,直到获得锁才能进一步操作,而恢复时又需要大量的等待时间。因此在一般的在购物类应用中,通过隔离级别来控制事务一致性的方式又被排除了,而对于脏读又风险过大,在大部分场景下,企业会选择读/写提交的方式设置事务,这样既有助于提高并发,又压制了脏读,但是对于数据一致性问题并没有解决,后面讨论解决。对于一般的应用都可以使用@Transactional方法进行配置。
隔离级别需要根据并发的大小和性能来做出决定,对于并发不大又要保证数据安全性的可以使用序列化的隔离级别,这样就能保证数据库在多事务环境中的一致性。
@Transactional(isolation = Isolation.SERIALIZABLE) public void save(Book book) { bookDao.insertBook(book); }
只是这样的代码会使得数据库的并发能力低下,在抢购商品的场景下出现卡顿的情况,所以在高并发的场景下这样的代码并不适用。
注解@Transaction
的默认隔离级别是Isolation.DEFAULT
,其含义是默认的,随数据库的默认值而变化。因为不同的数据库支持的隔离级别是不一样的,MySQL支持全部四种隔离级别,默认为可重复读的隔离级别。而Oracle只支持读/写提交和序列化,默认读/写提交.
传播行为
传播行为是指方法之间的调用事务策略问题。在大部分情况下,我们都希望事务能够同时成功或者同时失败。但也会有例外,假如现在需要实现信用卡的还款功能,有一个总的代码调用逻辑————RepaymentBatchService的batch方法,那么它要实现的是
记录还款成功的总卡数和对应完成的信息,而每一张卡的还款则是通过RepaymentService的repay方法完成的。
首先来分析业务。如果只有一条事务,那么当调用RepaymentService的repay方法对某一张信用卡进行还款时,不幸的事发生了,它发生了异常。如果将这条事务回滚,就会造成所有的数据操作都会回滚,那些已经正常还款的用户也会还款失败,这将是一个糟糕的结果。当batch方法调用repay方法时,它会为repay方法创建一个新的事务。当这个方法产生异常时,只会回滚它自身的事务,而不会影响主事务和其它事务,这样就避免了上面的问题
Spring中传播行为的类型是通过一个枚举去定义的org.springframework.transaction. annotation.Propagation
传播行为 | 含义 | 备注 |
REQUIRED |
当方法调用时,如果不存在当前事务,那么就创建事务;如果之前的方法已经存在事务了,那么就沿用之前的事务 |
spring默认 |
SUPPORTS | 支持当前事务,如果当前没有事务,就以非事务方式执行。 | |
MANDATORY | 方法必须在事务内运行 |
使用当前的事务, 如果当前没有事务,就抛出异常。 |
REQUIRES_NEW | 新建事务,如果当前存在事务,把当前事务挂起 | 事务管理器会打开新的事务运行该方法 |
NOT_SUPPORTED | 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起,直到该方法结束才恢复当前事务 | 适用于那些不需要事务的SQL |
NEVER | 以非事务方式执行,如果当前存在事务,则抛出异常。 | |
NESTED | 嵌套事务,也就是调用方法如果抛出异常只回滚自己内部执行的SQL,而不回滚主方法的SQL | 它的实现存在两种情况:如果当前数据库支持保存点(savepoint),那么它就会在当前事务上使用保存点技术;如果发生异常,则将方法内执行的SQL回滚到保存点上,而不是全部回滚;否则就等同于REQUIRES_NEW创建新的事务运行方法代码 |
一般而言,企业级应用中主要关注的是REQUEIRES_NEW和NESTED
@Transactional的自调用失效问题
注解@Transactional底层的实现是SpringAOP技术,而SpringAOP技术使用的是动态代理。这就意味着对于静态(static)方法和非public方法,注解@Transactional是失效的。还有一个更隐秘的,而且在使用过程中极其容易犯错的——自调用。
所谓自调用,就是
一个类的方法调用自身另外一个方法的过程
@Service public class BookServiceImpl implements BookService { @Autowired private BookMapper bookMapper; // 传播行为定义为REQUIRED @Override @Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRED,timeout = 1) public int insertBookList(List<Book> bookList) { int count=0; bookList.forEach(book-> count += this.insertBook(book));//调用自己的方法,产生自调用问题 return count; } // 传播行为定义为REQUIRES_NEW,每次调用产生新事务 @Override @Transactional(isolation = Isolation.READ_COMMITTED,
propagation = Propagation.REQUIRES_NEW,timeout = 1) public int insertBook(Book book) { return bookMapper.insertBook(book); } }
典型错误用法剖析
MVC模型中Controller中调用Service的问题
- 当一个Controller使用Service方法时,如果这个Service标注有@Transactional,那么它就会启用一个事务,而一个Service方法完成后,它就会释放该事务
- 当一个Controller声明的方法中,调用了两次Service方法,这两个方法是有各自单独的事务的。如果多次调用,且不在同一个事务中,这会造成不同时提交和回滚不一致的问题
防止过长时间占用事务
- 大型互联网系统中,一个数据库的链接可能也就是50条左右。
- @Transactional的Service类中的方法代码,作为一个事务整体,与数据库没有交互的代码,如网络请求,文件上传也会占用事务时间,因为只有方法运行完成后,返回Result后才会关系数据库资源
错误异常捕获语句
- MVC模型中服务类分为原子服务类和组合服务类
- ...方法已经存在异常了,由于开发者不了解Spring的事务约定,在两个操作方法里加入自己的try...catch..语句,就可能造成数据库操作发生异常的时候,被代码中的try...catch..所捕获,Spring在事务约定的流程中再也得不到任何异常信息,此时Spring就会提交事务,造成数据不一致
- 解决方法时将捕获的异常向上抛出