Spring事务管理

Spring数据库事务管理器的设计java

在Spring中数据库事务是经过PlatformTransactionManager进行管理的。附TransactionTemplate重要源码。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接口来完成的
  • 当事务产生异常时会回滚事务,在默认实现中全部的异常都会回滚。咱们能够经过配置去修改在某些异常发生时回滚或者不回滚事务
  • 当无异常时,提交事务

配置事务管理器express

目前咱们讨论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中可使用声明式事务和编程式事务,现在编程式事务几乎不用了,由于它会产生冗余,代码可读性较差。声明式事务又能够分为xml配置和注解事务,但xml方式已经不经常使用了,因此这里只简单交代下它的用法,目前主流方法是注解@Transactional。安全

声明式事务网络

声明式事务是一种约定性的事务,在大部分的状况下,使用数据库事务时,大部分场景是在代码中发生了异常时,须要回滚事务,而不发生异常时则是提交事务,从而保证数据库数据的一致性。从这点出发,Spring给了一个约定(相似于AOP开发给的约定),你的业务方法不发生异常,事务管理器就提交事务,发生异常则让事务管理器回滚事务。并发

首先声明式事务容许自定义事务接口——TransactionDefinition,它能够由xml或者注解@Transactional进行配置,到了这里咱们先谈谈@Transactional的配置项app

@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 {};  
}  

Transactional的配置项框架

 

配置项 含义 备注
value 定义事务管理器

spring容器中的一个Bean id,

这个Bean须要实现接口PlatformTransactionManager

transactionManager 同上 同上
isolation 隔离级别 数据库在并发事务时的概念,默认取值为数据库的隔离级别
propagation 传播行为 方法之间调用问题,默认取值为Propagation.REQUIRED
timeout 超时时间 单位为秒,当超时时,会引起异常,默认会致使事务回滚
readOnly 是否开启只读事务 默认值:false
rollbackFor 回滚事务的异常类定义 只有当方法产生所定义的异常时,才会回滚事务
rollbackForClassName 回滚事务的异常类名定义 同rollbackFor,只是使用类名称定义
noRollbackFor 当产生哪些异常不回滚事务 当产生所定义异常时,Spring将继续提交事务
noRollbackForClassName 同noRollbackFor 同noRollbackFor,只是使用类名称定义

只须要在xml配置文件中加入以下配置就可使用@EnableTransactionManagement@Transactional配置事务了

<tx:annotation-driven transaction-manager="txManager"/>

** 使用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>  

选择隔离级别

在互联网应用中,不但要考虑数据库数据的一致性,并且还要考虑系统的性能,通常而言,从脏读到序列化,系统性能直线降低。所以设置高的级别,好比序列化,会严重压制并发,从而引起大量的线程挂起,直到得到锁才能进一步操做,而恢复时又须要大量的等待时间。所以在通常的在购物类应用中,经过隔离级别来控制事务一致性的方式又被排除了,而对于脏读又风险过大,在大部分场景下,企业会选择读/写提交的方式设置事务,这样既有助于提升并发,又压制了脏读,可是对于数据一致性问题并无解决,后面讨论解决。对于通常的应用均可以使用@Transactional方法进行配置。

隔离级别须要根据并发的大小和性能来作出决定,对于并发不大又要保证数据安全性的可使用序列化的隔离级别,这样就能保证数据库在多事务环境中的一致性。

@Transactional(isolation = Isolation.SERIALIZABLE)  
public void save(Book book) {  
    bookDao.insertBook(book);  
}  

只是这样的代码会使得数据库的并发能力低下,在抢购商品的场景下出现卡顿的状况,因此在高并发的场景下这样的代码并不适用。

注解@Transactional的默认隔离级别是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 它的实现存在两种状况:1.若是当前数据库支持保存点(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);  
    }  
}  

经过测试,在insertBook上标注的@Transactional失效了,这是一个很容易掉进去的陷阱。

出现这个问题的根本缘由在于AOP的实现原理。@Transactional实现原理是AOP,而AOP的实现原理是动态代理,类中的方法调用本身的另外一个方法,并不存在代理对象的调用,这样就不会产生AOP去为咱们设置@Transactional配置的参数,这样就出现了自调用注解失效的问题。为了解决这个问题,一方面咱们可使用两个服务类,另一方面能够直接从容器中获取service的代理对象。

典型错误用法剖析

MVC模型中Controller中调用Service的问题

  • 当一个Controller使用Service方法时,若是这个Service标注有@Transactional,那么它就会启用一个事务,而一个Service方法完成后,它就会释放该事务
  • 当一个Controller声明的方法中,调用了两次Service方法,这两个方法是有各自单独的事务的。若是屡次调用,且不在同一个事务中,这会形成不一样时提交和回滚不一致的问题

防止过长时间占用事务

  • 大型互联网系统中,一个数据库的连接可能也就是50条左右。
  • @Transactional的Service类中的方法代码,做为一个事务总体,与数据库没有交互的代码,如网络请求,文件上传也会占用事务时间,由于只有方法运行完成后,返回Result后才会关系数据库资源

错误异常捕获语句

  • MVC模型中服务类分为原子服务类和组合服务类
  • ...方法已经存在异常了,因为开发者不了解Spring的事务约定,在两个操做方法里加入本身的try...catch..语句,就可能形成数据库操做发生异常的时候,被代码中的try...catch..所捕获,Spring在事务约定的流程中再也得不到任何异常信息,此时Spring就会提交事务,形成数据不一致
  • 解决方法时将捕获的异常向上抛出
相关文章
相关标签/搜索