Spring事务管理详解

Spring事务介绍

Spring并不直接管理事务,而是提供了多种事务管理器,他们将事务管理的职责委托给Hibernate或者JTA等持久化机制所提供的相关平台框架的事务来实现。

Spring事务管理器的接口是org.springframework.transaction.PlatformTransactionManager,通过这个接口,Spring为各个ORM平台如JDBC、Hibernate、MyBatis等都提供了对应的事务管理器,但是具体的实现就是各个平台自己的事情了。

从这里可知具体的具体的事务管理机制对Spring来说是透明的,它并不关心那些,那些是对应各个平台需要关心的,所以Spring事务管理的一个优点就是为不同的事务API提供一致的编程模型,如JTA、JDBC、Hibernate、JPA。下面分别介绍各个平台框架实现事务管理的机制。

相关接口介绍:

spring主要通过以下三个接口对事务进行管理:

1、PlatformTransactionManager 平台事务管理器

public interface PlatformTransactionManager {
    // 返回一个已经激活的事务或创建一个新的事务(根据给定的TransactionDefinition类型参数定义的事务属性),
    // 返回的是TransactionStatus对象代表了当前事务的状态,其中该方法抛出TransactionException(未检查异常)
    // 表示事务由于某种原因失败。
    TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;

    // 提交给定的事务,检查其状态。如果事务被标记为rollback-only,则执行回滚。
    // 如果事务不是新的事务,则忽略提交周围适当的事务。如果先前的事务被挂起,则在提交新事务后恢复先前的事务。
    void commit(TransactionStatus status) throws TransactionException;

    // 执行给定事务的回滚。如果事务不是一个新的事务,将其周边适当的事务标记为rollback-only。
    // 如果先前的事务被挂起,则在回滚新事务后恢复先前的事务。
    void rollback(TransactionStatus status) throws TransactionException;
}

它的具体实现类:

DataSourceTransactionManager(Spring JDBC或iBatis持久化数据时)

HibernateTransactionManager(Hibernate3.0持久化数据时)

JdoTransactionManager(持久化机制为Jdo时)

JpaTransactionManager (JPA持久化时)

........

Spring为不同的ORM持久化框架提供了不同PlatformTransactionManager接口实现,具体实现则是由不同的ORM框架完成。

2、TransactionDefinition 事务定义信息(隔离、传播、超时、只读)

public interface TransactionDefinition {
    // 返回事务传播行为
    int getPropagationBehavior();

    // 返回事务隔离级别
    int getIsolationLevel();

    // 返回事务超时时间
    int getTimeout();

    // 返回事务是否只读
    boolean isReadOnly();

    // 返回事务的名称
    String getName();
}

3、TransactionStatus 事务具体运行状态

public interface TransactionStatus extends SavepointManager {
    // 返回当前事务是否是新事物
    boolean isNewTransaction();

    // 返回当前事务是否有保存点,即基于保存点创建的嵌套事务
    boolean hasSavepoint();

    // 标记事务为rollback-only。这将指示事务管理器,事务的唯一可能结果是回滚,
    // 作为抛出异常的替代方法,该异常将反过来触发回滚。
    void setRollbackOnly();

    // 返回事务是否是被标记为rollback-only
    boolean isRollbackOnly();

    // 如果适用,将底层会话刷新到数据存储区:例如,所有受影响的Hibernate/JPA会话。
    void flush();
    
    // 返回事务是否完成(提交或回滚)
    boolean isCompleted();
}



事务属性

在定义事务之前,需要了解一些事务的参数,正如前边TransactionDefinition类定义的,包括隔离级别、传播机制、事务超时、是否只读等,还包括回滚规则定义等参数。

隔离级别

隔离级别定义一个事务可能接受其他并发事务活动受影响的程度。也可以想象成事务对于数据处理的自私程度。它是多个事务并发执行时,产生的。

多个事务同时运行,经常会为了完成他们的工作而操作同一个数据。并发虽然是必需的,但是会导致以下问题:

  • 胀读——A事务读取数据并修改,未提交之间B事务又读取了数据
  • 不可重复读——A事务读取数据,B事务读取数据并修改,A事务再读取数据时发现两次数据不一致
  • 幻读——事务A读取或删除了全部数据,事务B又insert一条数据,这时事务A发现莫名其妙的多了一条数据

理想状态下,事务之间是完全隔离的。但是完全隔离会影响性能,因为隔离的实现依赖于数据库中的锁,侵占性锁会阻碍并发,要求事务互相等待。

考虑到完全隔离会影响性能,而且并不是所有的情况都要求完全隔离,所以有时候可以在事务隔离方面灵活处理。因此,就有好几个隔离级别。

Spring事务管理的隔离级别

解决事务的安全性问题:脏读、不可重复读、幻读。下面是Spring事务的隔离级别:

隔离级别 含义
DEFAULT 使用数据库默认的隔离级别(spring中的选择项)
READ_UNCOMMITTED 允许你读取还未提交的改变了的数据。可能导致脏、幻、不可重复读
READ_COMMITTED 允许在并发事务已经提交后读取。可防止脏读, 但幻读、不可重复读仍可能发生(Oracle默认的隔离级别)
REPEATABLE_READ 对相同字段的多次读取是一致的,除数据对事务本身改变。可防止脏、不可重复读,但幻读仍可能发生 (MySQL默认的隔离级别)
SERIALIZABLE 完全服从ACID的隔离级别,确保不发生脏、幻、不可重复读。这在所有的隔离级别中是最慢的,它是典型的通过完全锁定在事务中涉及的数据表来完成的。


事务传播行为

当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。在TransactionDefinition定义中包括了如下7个表示传播行为的常量:

传播行为 说明
PROPAGATION_REQUIRED 业务方法需要在一个事务中运行。如果方法运行时,已经处在一个事务中,那么加入到该事务,否则为自己创建一个新的事务
PROPAGATION_NOT_SUPPORTED 声明方法不需要事务。如果方法没有关联到一个事务,容器不会为它开启事务。如果方法在一个事务中被调用,该事务会被挂起,在方法调用结束后,原先的事务便会恢复执行
PROPAGATION_REQUIRES_NEW 属性表明不管是否存在事务,业务方法总会为自己发起一个新的事务。如果方法已经运行在一个事务中,则原有事务会被挂起,新的事务会被创建,直到方法执行结束,新事务才算结束,原先的事务才会恢复执行
PROPAGATION_MANDATORY 该属性指定业务方法只能在一个已经存在的事务中执行,业务方法不能发起自己的事务。如果业务方法在没有事务的环境下调用,容器就会抛出异常。
PROPAGATION_SUPPORTS 这一事务属性表明,方法可以受事务控制,也可以不。如果业务方法在某个事务范围内被调用,则方法成为该事务的一部分。如果业务方法在事务范围外被调用,则方法在没有事务的环境下执行
PROPAGATION_NEVER 指定业务方法绝对不能在事务范围内执行。如果业务方法在某个事务中执行,容器会抛出异常,只有业务方法没有关联到任何事务,才能正常执行
PROPAGATION_NESTED 如果一个活动的事务存在,则运行在一个嵌套的事务中. 如果没有活动事务, 则按REQUIRED属性执行.它使用了一个单独的事务, 这个事务拥有多个可以回滚的保存点。内部事务的回滚不会对外部事务造成影响。它只对DataSourceTransactionManager事务管理器起效

注意REQUIRES_NEW和NESTED两者的区别;
PROPAGATION_REQUIRES_NEW启动一个新的, 不依赖于环境的 "内部" 事务. 这个事务将被完全 commited 或 rolled back 而不依赖于外部事务, 它拥有自己的隔离范围, 自己的锁, 等等. 当内部事务开始执行时, 外部事务将被挂起, 内务事务结束时, 外部事务将继续执行。

PROPAGATION_NESTED 开始一个 "嵌套的" 事务, 它是已经存在事务的一个真正的子事务. 潜套事务开始执行时, 它将取得一个 savepoint. 如果这个嵌套事务失败, 我们将回滚到此 savepoint. 潜套事务是外部事务的一部分, 只有外部事务结束后它才会被提交。

对嵌套事务的理解?
嵌套:是子事务在父事务中执行,子事务是父事务的一部分,在进入子事务之前,父事务建立一个回滚点,叫save point,然后执行子事务,这个子事务也算是父事务的一部分,然后子事务执行结束,父事务继续执行。下面看几个问题就懂了

如果子事务回滚,会发生什么?
答:父事务会回滚到save point,然后尝试其他的事务或者其他的业务逻辑,父事务之前的操作不会受到影响,更不会自动回滚

如果父事务回滚,会发生什么?
答:父事务回滚,子事务也会跟着回滚。父事务结束之前,子事务不会提交。

嵌套事务的提交是什么情况?
答:子事务是父事务的一部分,所以子事务先提交,父事务再提交


事务超时

假设事务的运行时间变得格外的长,由于事务可能涉及对后端数据库的锁定,所以长时间运行的事务会不必要地占用数据库资源,这时就可以声明一个事务在特定时间后自动回滚。

由于超时时钟在一个事务启动的时候开始的,因此,有对于那些具有可能启动一个新事物的传播行为(PROPAGATION_REQUIRED,PROPAGATION_REQUIRES_NEW,PROPAGATION_NESTED)的方法来说,声明事务超时才有意义,默认为30S。

是否只读

如果事务只对后端数据进行读操作,那么如果将事务设置为只读事务,可以利用后端数据库优化措施进行适当优化。

只读事务”并不是一个强制选项,它只是一个“暗示”,提示数据库驱动程序和数据库系统,这个事务并不包含更改数据的操作,那么JDBC驱动程序和数据库就有可能根据这种情况对该事务进行一些特定的优化,比方说不安排相应的数据库锁,以减轻事务对数据库的压力,毕竟事务也是要消耗数据库的资源的。但是你非要在“只读事务”里面修改数据,也并非不可以,只不过对于数据一致性的保护不像“读写事务”那样保险而已。

因此,“只读事务”仅仅是一个性能优化的推荐配置而已,并非强制你要这样做不可。

只读事务实在开启事务时有数据库实施的,所以只对具备启动新事务的传播机制有效,如REQUIRED、REQUIRES_NEW、NESTED。

回滚规则

回滚规则定义了哪些异常引起回滚,哪些不引起。在默认情况下,事务只出现运行时异常(Runtime Exception)时回滚,而在出现受检查异常(Checked Exception)时不回滚。

但是我们可以在Spring中进行定义来改变其默认行为。Spring在xml文件配置事务时提供了rollback-for和no-rollback-for参数,来指定回滚和不会滚的异常名称,该名称对应的类为Throwable的子类。



Spring的事务支持

Spring提供了对编程式事务和声明式事务的支持,编程式事务允许用户在代码中精确定义事务的边界,而声明式事务(基于AOP)有助于用户将操作与事务规则进行解耦。

简单地说,编程式事务侵入到了业务代码里面,但是提供了更加详细的事务管理,实现细粒度的事务控制;而声明式事务由于基于AOP,所以既能起到事务管理的作用,又可以不影响业务代码的具体实现。

如果你并不需要细粒度的事务控制,你可以使用声明式事务,在Spring中,你只需要在Spring配置文件中做一些配置,即可将操作纳入到事务管理中, 解除了和代码的耦合, 这是 对应用代码影响最小的选择,从这一点再次验证了Spring关于 AOP的概念。当你不需要事务管理的时候,可以直接从Spring配置文件中移除该设置。

编程式事务

Spring提供了TransactionTemplate工具类,可以方便地让开发人员手动的编程实现事务控制。编程式事务虽然可以精确控制事务,但是事务控制代码必须侵入业务逻辑代码中,耦合度高,后期难以维护。一般而言,不需要精确控制事务,所以采用的更多的是Spring的声明式事务。

声明式事务

Spring声明式事务基于AOP实现,有两种事务定义方式:xml配置和注解定义,前者使用tx命名空间,后者使用@Transactional注解。

由于声明式事务是基于AOP实现的,所以只能实现对方法的粗粒度的控制,而无法做到代码块级别的细粒度控制。

使用声明式事务还是编程式事务管理,在很大程度上是 细粒度和易用性之间权衡。

上一篇:TOC制约理论在项目管理中的应用案例分析


下一篇:分析至上 大数据项目部署的五大愿景