深入理解Spring事务

625 阅读9分钟

深入理解Spring事务

事务的特性

  • 原子性

事务是一个原子操作,由一系列动作组成。事务的原子性确保动作要么全部完成,要么完全不起作用。

  • 一致性

一旦事务完成(不管成功还是失败),系统必须确保它所建模的业务处于一致的状态,而不会是部分完成部分失败。在现实中的数据不应该被破坏。

  • 隔离性

可能有许多事务会同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。

  • 持久性

一旦事务完成,无论发生什么系统错误,它的结果都不应该受到影响,这样就能从任何系统崩溃中恢复过来。通常情况下,事务的结果被写到持久化存储器中。

事务的传播

事务传播行为类型说明
PROPAGATION_REQUIRED如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。
PROPAGATION_SUPPORTS支持当前事务,如果当前没有事务,就以非事务方式执行。
PROPAGATION_MANDATORY使用当前的事务,如果当前没有事务,就抛出异常。
PROPAGATION_REQUIRES_NEW新建事务,如果当前存在事务,把当前事务挂起。
PROPAGATION_NOT_SUPPORTED以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER以非事务方式执行,如果当前存在事务,则抛出异常。
PROPAGATION_NESTED如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。

传播规则回答了这样一个问题:一个新的事务应该被启动还是被挂起,或者是一个方法是否应该在事务性上下文中运行。

事务的隔离级别

隔离级别含义
ISOLATION_DEFAULT使用后端数据库默认的隔离级别
ISOLATION_READ_UNCOMMITTED允许读取尚未提交的更改。可能导致脏读、幻读或不可重复读。
ISOLATION_READ_COMMITTED(Oracle 默认级别)允许从已经提交的并发事务读取。可防止脏读,但幻读和不可重复读仍可能会发生。
ISOLATION_REPEATABLE_READ(MYSQL默认级别)对相同字段的多次读取的结果是一致的,除非数据被当前事务本身改变。可防止脏读和不可重复读,但幻读仍可能发生。
ISOLATION_SERIALIZABLE完全服从ACID的隔离级别,确保不发生脏读、不可重复读和幻影读。这在所有隔离级别中也是最慢的,因为它通常是通过完全锁定当前事务所涉及的数据表来完成的。

事务并发将会产生的问题

  1. 脏读(Dirty read)

脏读发生在一个事务读取了被另一个事务改写但尚未提交的数据时。如果这些改变在稍后被回滚了,那么第一个事务读取的数据就会是无效的。

  1. 不可重复读(Nonrepeatable read)

不可重复读发生在一个事务执行相同的查询两次或两次以上,但每次查询结果都不相同时。这通常是由于另一个并发事务在两次查询之间更新了数据。

  1. 幻读(Phantom reads)

幻读和不可重复读相似。当一个事务(T1)读取几行记录后,另一个并发事务(T2)插入了一些记录时,幻读就发生了。在后来的查询中,第一个事务(T1)就会发现一些原来没有的额外记录。

Spring事务的配置方式

编程式事务管理

编程式事务管理是侵入性事务管理,使用TransactionTemplate或者直接使用PlatformTransactionManager,对于编程式事务管理,Spring推荐使用TransactionTemplate。

  • 编程式事务样例

public class BussinessServiceImpl implements BussinessService {
  private BussinessDao bussinessDao;
  private TransactionTemplate transactionTemplate;
  ......
  public boolean transfer(final Long fromId, final Long toId, final double amount) {
    //调用一个回调函数
    return (Boolean) transactionTemplate.execute(new TransactionCallback(){
  public Object doInTransaction(TransactionStatus status) {
    Object result;
    try {
      result = bussinessDao.transfer(fromId, toId, amount);
    } catch (Exception e) {
    status.setRollbackOnly();
    result = false;
    System.out.println("Transfer Error!");
    }
    return result;
    }
    });
  }
}

声明式事务管理

声明式事务管理建立在AOP之上,其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,执行完目标方法之后根据执行的情况提交或者回滚。 编程式事务每次实现都要单独实现,但业务量大功能复杂时,使用编程式事务无疑是痛苦的,而声明式事务不同,声明式事务属于无侵入式,不会影响业务逻辑的实现,只需要在配置文件中做相关的事务规则声明或者通过注解的方式,便可以将事务规则应用到业务逻辑中。 显然声明式事务管理要优于编程式事务管理,这正是Spring倡导的非侵入式的编程方式。唯一不足的地方就是声明式事务管理的粒度是方法级别,而编程式事务管理是可以到代码块的,但是可以通过提取方法的方式完成声明式事务管理的配置。

  • 使用XML进行配置事务管理器

  1. 配置事务拦截器
<!--配置事务拦截器-->
<bean id="transactionInterceptor" class="org.springframework.transaction.interceptor.TransactionInterceptor">
    <property name="transactionManager" ref="transactionManager"/>
    <!-- 配置事务属性 -->
    <property name="transactionAttributes">
        <props>
            <!-- key代表的是业务方法的正则式匹配 ,而其内容可以配置各类事务定义参数-->
            <prop key="insert*">PROPAGATION_REQUIRED,ISOLATION_READ_UNCOMMITTED</prop>
            <prop key="save*">PROPAGATION_REQUIRED,ISOLATION_READ_ UNCOMMITTED</prop>
            <prop key="add*">PROPAGATION_REQUIRED,ISOLATION_READ_ UNCOMMITTED</prop>
            <prop key="select*">PROPAGATION_REQUIRED,readOnly</prop>
            <prop key="get*">PROPAGATION_REQUIRED,readOnly</prop>
            <prop key="find*">PROPAGATION_REQUIRED,readOnly</prop>
            <prop key="del*">PROPAGATION_REQUIRED,ISOLATION_READ_ UNCOMMITTED</prop>
            <prop key="remove*">PROPAGATION_REQUIRED,ISOLATION_READ_ UNCOMMITTED</prop>
            <prop key="update*">PROPAGATION_REQUIRED,ISOLATION_READ_ UNCOMMITTED</prop>
        </props>
    </property>
</bean>
  1. 指明事务拦截器拦截哪些类
<!--指明事务拦截器拦截哪些类-->
<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
    <property name="beanNames">
        <list>
            <value>*ServiceImpl</value>
        </list>
    </property>
    <property name="interceptorNames">
        <list>
            <value>transactionInterceptor</value>
        </list>
    </property>
</bean>

使用注解进行事务配置

@Transactional(propagation = Propagation.REQUIRED)
public boolean transfer(Long fromId, Long transactionId, double num) {
  return bussinessDao.transfer(fromId, transactionId, num);
}

Spring 事务管理 API

事务管理,实质上就是按照给定的事务规则来执行提交或者回滚操作

PlatformTransactionManager接口

Spring事务策略是通过PlatformTransactionManager接口体现的,接口源码如下:

public interface PlatformTransactionManager {

    //平台无关的获得事务的方法
    TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;

    //平台无关的事务提交方法
    void commit(TransactionStatus status) throws TransactionException;

    //平台无关的事务回滚方法
    void rollback(TransactionStatus status) throws TransactionException;
}
  • 在PlatformTransactionManager接口内,包含一个getTransaction(TransactionDefinition definition)方法,该方法根据一个TransactionDefinition参数,返回一个TransactionStatus对象。TransactionStatus对象表示一个事务,该事务可能是一个新的事务,也可能是一个已经存在的事务对象,这由TransactionDefinition所定义的事务规则所决定。

TransactionDefinition 接口

TransactionDefinition 接口用于定义一个事务的规则,它包含了事务的一些静态属性,比如:事务传播行为、超时时间等。同时,Spring 还为我们提供了一个默认的实现类:DefaultTransactionDefinition,该类适用于大多数情况。如果该类不能满足需求,可以通过实现 TransactionDefinition 接口来实现自己的事务定义。 接口源码如下:

public interface TransactionDefinition{
    //事务的隔离级别
    int getIsolationLevel();
    //事务的传播
    int getPropagationBehavior();
    //事务超时
    int getTimeout();
    //事务的只读属性
    boolean isReadOnly();
}
  • TransactionDefinition 接口定义的事务规则包括:事务隔离级别、事务传播行为、事务超时、事务的只读属性和事务的回滚规则
  • 事务的隔离,事务的传播前面已经阐述过,便不再赘述
  • 事务超时:就是指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。
  • 事务的只读属性:对事务性资源进行只读操作或者是读写操作。所谓事务性资源就是指那些被事务管理的资源,比如数据源、 JMS 资源,以及自定义的事务性资源等等。
  • 事务的回滚规则:通常情况下,如果在事务中抛出了未检查异常(继承自 RuntimeException 的异常),则默认将回滚事务。如果没有抛出任何异常,或者抛出了已检查异常,则仍然提交事务。

TransactionStatus 接口

TransactionStatus 接口提供了一个简单的控制事务执行和查询事务状态的方法。该接口的源代码如下:

public  interface TransactionStatus{
   boolean isNewTransaction();
   void setRollbackOnly();
   boolean isRollbackOnly();
}

事务的回滚

编程式事务回滚

显式在代码逻辑中捕获异常或者其他判断逻辑执行回滚,因此是不会出现不回滚的现象

声明式事务回滚

当被切面切中或者是加了注解的方法中抛出了RuntimeException异常时,Spring会进行事务回滚。默认情况下是捕获到方法的RuntimeException异常,也就是说抛出只要属于运行时的异常(即RuntimeException及其子类)都能回滚;但当抛出一个不属于运行时异常时,事务是不会回滚的。

声明式事务中常见的不回滚情况

  1. 声明式事务配置切入点表达式写错了,没切中Service中的方法
  2. Service方法中,把异常给try catch了,但catch里面只是打印了异常信息,没有手动抛出RuntimeException异常
  3. Service方法中,抛出的异常不属于运行时异常(如IO异常),因为Spring默认情况下是捕获到运行时异常就回滚

如何保证事务回滚

  1. 如果Service层会抛出不属于运行时异常也要能回滚,那么可以将Spring默认的回滚时的异常修改为Exception,这样就可以保证碰到什么异常都可以回滚,配置方式如下:
  • 声明式事务,在配置里面添加一个rollback-for
 <tx:method name="update*" propagation="REQUIRED" rollback-for="java.lang.Exception"/> 
  • 注解事务,直接在注解上面指定rollback-for参数
@Transactional(rollbackFor=Exception.class)
  1. 只有非只读事务才能回滚的,只读事务是不会回滚的
  2. 如果在Service层用了try catch,在catch里面再抛出一个 RuntimeException异常,这样出了异常才会回滚
  3. 如果不想采用第三种解决办法,可以在catch后面写一句回滚代码来实现回滚,这样的话,就可以在抛异常后也能return 返回值;比较适合需要拿到Service层的返回值的场景,如下所示
       @Transactional(rollbackFor = { Exception.class })  
       public boolean test() {  
            try {  
               doDbSomeThing();    
            } catch (Exception e) {  
                 e.printStackTrace();     
                 // 加上之后抛了异常就能回滚(有这句代码就不需要再手动抛出运行时异常了)
                 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();  
                 return false;
            }  
           return true;
      }