Spring事务相关问题

一、事务
    在软件开发领域,全有或者全无的操作被称之为事务(Transaction)。
    事务允许将多个操作组合成一个要么全部发生要么全部不发生的工作单元
    在事务的执行过程中,若组成事务的多个操作均顺利执行成功,那么该事务就执行成功;若组成事务的多个操作中有某一个操作出现异常而没有执行成功,那么将会执行事务回滚,该事务中所包含的所有操作将被完全清除,就像什么事情也没有发生一样。

二、举例理解事务
    例如买电影票的场景。购买电影票的操作中大概涉及到以下几个行为:
    ① 检查空闲座位的数量,确保有足够的空位;
    ② 每卖出一张电影票,空闲座位数量必须减少一个;
    ③ 为购买的电影票进行支付;
    ④ 将电影票分配给购票者;
    如果以上几个行为均顺利执行的话,购票者将成功买得电影票;但如果这些行为中的某一个行为出错,例如购票者在支付时发现余额不足、或者最后一步时分配电影票失败;那么电影院空闲座位数、购买者账户余额都应该保持本次购票动作之前的原始数据;否则将会出现:购票者账户余额不足支付失败但购票成功!!! 电影院将会人为地消耗掉一个座位,继而影响销售;分配电影票失败,购买者花了钱却没有买到电影票!!!将会给消费者造成经济损失;
    为了保证购买者和影院都不会有损失,这些操作应该包装到一个事务中。作为事务,这些过程将被视为一个操作,从而保证所有操作要么全都成功,要么全部回滚,就像从未发生过。
    在软件中,事务扮演了重要的角色,它确保了数据或资源免于处在不一致的状态中。如果没有它们的化,可能会损坏数据或者导致应用程序业务规则的不一致。

三、事务四要素
    在传统软件开发中,人们创建了一个术语来描述事务:ACID;这个术语表示事务的4个特性:
1. 原子性(Atomic):事务是由一个或多个活动所组成的一个工作单元。原子性确保事务中的所有操作全部发生或者全部不发生。如果所有的活动都成功了,事务也就成功了;如果其中任意一个活动失败了,真个事务也就执行失败并回滚;
2. 一致性(Consistent):一旦事务完成(不论是成功还是失败),系统必须确保它所建模的业务处于一致的状态。现实的数据不应该被破坏;
3. 隔离性(Isolated):事务允许多个用户对相同的数据进行操作,每个用户的操作不会与其他用户纠缠在一起。因此,事务应该被彼此隔离,避免发生同步读写相同数据的事情(需要注意的是,隔离性往往涉及到锁定数据库中的行或表);
4. 持久性(Durable):一旦事务完成,事务的执行结果应该持久化,这样就能从任何的系统崩溃中恢复过来;这一般会涉及将结果存储到数据库或其他形式的持久化存储中。
    例如,在前面购买电影票的例子中,当任意一个步骤失败时,所有步骤的操作结果都会被取消,从而保证了事务的原子性;原子性通过保证系统数据永远不会处于不一致或部分完成的状态来确保一致性;隔离性同样确保了一致性,它是通过在购票者购票期间不允许其他同步事务获取购票者的座位达到这一点的。
    最后,所做的结果是持久化的,因为它们会被提交到某种数据存储中;在发生系统崩溃或其他灾难性事情的时候,不用担心事务的结果会丢失。

四、Spring对事务管理的支持
    Spring提供了对编码式和声明式事务管理的支持。Spring通过回调机制将实际的事务实现从事务性的代码中抽象出来;如果应用程序只需要使用一种持久化资源,Spring可以使用持久化机制本身所提供的事务性支持,这包括了JDBC、Hibernate、以及Java持久化API(Java Persistence API, JPA)。但是如果应用程序的事务跨多个资源,那么Spring会使用第三方的JTA实现来支持分布式(XA)事务。
    编码式事务允许用户在代码中精确定义事务的边界,而声明式事务(基于AOP)有助于用户将操作与事务规则进行解耦。Spring对声明式事务的支持会让人联想起EJB的容器管理事务(container-managed transaction,CMT)。它们都允许声明式地定义事务边界。但是Spring的声明式事务要胜过CMT,因为它允许声明额外的一些属性,例如隔离级别和超时。
    选择编码式事务还是声明式事务很大程度上是在细粒度控制和易用性之间进行权衡。当通过编码实现事务控制时,能够精确控制事务的边界,它们的开始和结束完全取决于业务需求。通常,不需要编码是事务所提供的细粒度控制,而会选择在上下文定义文件中声明事务。
    无论选择将事务编码到Bean中还是选择将其定义为切面,都需要使用Spring事务管理器与平台相关的事务进行交互。

五、Spring中的编码式事务
    开发者想要完全的控制事务从哪里开始、在哪里提交、在哪里结束,则声明式事务是不够精确的;声明式事务只能在方法级别声明事务的边界,如果想要更好的控制事务边界,那么只能选择编码式事务;

public void saveUser(User user){
    userDao.createUser(user);
}

    尽管这个方法看起来很简单,但它不仅仅是呈现出来的样子;当 User 保存的时候,底层的持久化机制会做很多的事情。尽管最终仅仅是往数据库中插入一行数据,但是要保证发生的所有事情都在事务中是很重要的。如果成功,所有的工作都会被提交;如果失败,事务将会回滚。

    添加事务的一种方式是在 saveUser()方法中直接通过编码使用 Spring的 TransactionTemplate 来添加事务性边界。就像 Spring 中的其他模板类,TransactionTemplate 使用了一种回调机制。如下所示代码,展现了如何使用 TransactionTemplate 来添加事务性上下文:

public void saveUser(final User user){
    txTemplate.execute(new TransactionCallback<Viod>() {
	    public Viod doInTransaction(TransactionStatus txStatus){
		    try{
			    userDao.createUser(user);
		    } catch (RuntimeException e){
			    txStatus.setRollbackOnly();
			    throw e;
		    }
		    return null;
	    }
    });
}

    为了使用 TransactionTemplate,需要实现 TransactionCallback 接口。因为 TransactionCallback 只有一个要实现的方法,通常会很简单地将其实现为匿名内部类,就像如上所示代码实现,对于事务性的代码,将其放在 doInTransaction()方法中。

    调用 TransactionTemplate 实例的 execute() 方法时,将会执行 TransactionCallback 实例中的的代码。如果执行过程中遇到了问题,调用 TransactionStatus 对象的 setRollbackOnly() 方法将回滚事务。否则,如果 doInTransaction() 成功返回,事务将会提交。
    TransactionTemplate 实例是从哪里来的呢?它需要注入到 UserServiceImpl 中,示例如下:

    <bean id="userService" class="com.xxx.xxx.UserServiceImpl">
		...
		<property name="transactionTemplate">
			<bean class="org.springframework.transaction.support.TransactionTemplate">
				<property name="transactionManager" ref="transactionManager"/>
			</bean>
		</property>
	</bean>

    需要注意的是,TransactionTemplate 注入了一个 transactionManager,TransactionTemplate 使用了 PlatformTransactionManager 实现来处理特定平台的事务管理细节。这里装配了一个名为 transactionManager 的 Bean引用,它可以是任意一个事务管理器。
    如果想完全控制事务的边界,编码式事务是很好的。但是,从以上编码式实现案例中可以看出它是侵入性的。那么必须修改 saveUser() 实现(使用Spring的特定类)来得到Spring的编码式事务的支持。
    编程式事务每次实现都要单独实现,但对于业务量大功能复杂的业务,使用编程式事务无疑是痛苦的,而声明式事务不同,声明式事务属于无侵入式,不会影响业务逻辑的实现,只需要在配置文件中做相关的事务规则声明或者通过注解的方式,便可以将事务规则应用到业务逻辑中。
    显然声明式事务管理要优于编程式事务管理,这正是Spring倡导的非侵入式的编程方式。通常情况下,事务需求不会要求如此精确的事务边界控制。这就是为什么通常会将事务声明放在应用程序代码之外(例如在Spring配置文件中)。唯一不足的地方就是声明式事务管理的粒度是方法级别的,而编程式事务管理是可以精确到代码块的,但是可以通过提取方法的方式完成声明式事务管理的配置。

六、声明式事务
    在以前声明式事务还是EJB容器所特有的;但现在,Spring 为 POJO 提供了声明式事务的支持。这是Spring的一个重要特性,因为除了EJB,现在有实现声明式原子操作的替代方案了。
    Spring对声明式事务的支持是通过使用 Spring AOP 框架实现的;其本质是对方法前后拦截,然后在目标方法开始之前创建或者加入一个事务,执行完目标方法之后根据执行的情况对执行结果进行提交或者回滚。
    Spring提供了3种方式来声明事务边界。以前,Spring只能使用Spring AOP 和 TransactionProxyFactoryBean 的代理Bean来实现声明式事务。但是从 Spring2.0 声明式事务的更好方式是使用Spring的 tx 命名空间和 @Transactional 注解。
1. 定义事务属性
    在Spring中,声明式事务是通过事务属性(tarnsaction attribute)来定义的。事务属性描述了事务策略如何应用到方法上。
    事务属性包含了5个方面:传播行为、隔离级别、回滚规则、事务超时、是否只读。声明式事务通过这5个事务属性来进行定义。
    尽管Spring提供了多种声明式事务的机制,但是所有的方式都依赖于这五个参数来控制如何管理事务策略。因此,如果要在Spring中声明事务策略,就要理解这些参数。
(1) 事务传播行为:    
    事务的第一个方面是传播行为(propagation behavior)。传播行为定义了客户端与被调用方法之间的事务边界。
    事务的传播性一般用在事务嵌套的场景,比如一个事务方法里面调用了另一个事务方法,那么两个方法是各自作为独立的方法提交还是内层的事务合并到外层的事务一起提交,这就需要事务传播机制的配置来确定怎样执行。

Spring定义了7中不同的传播行为:

事务传播行为含义
PROPAGATION_MANDATORY表示该方法必须在事务中运行。如果当前事务不存在,则会抛出一个异常。
PROPAGATION_NESTED

表示如果当前已经存在一个事务,那么该方法将会在嵌套事务中运行。嵌套的事务可以独立于当前事务进行单独的提交或者回滚。

如果当前事务不存在,那么其行为与 PROPAGATION_REQUIRED一样。注意各厂商对这种传播行为的支持是有所差异的。可以参考资源管理器的文档来确定它们是否支持嵌套式事务。

PROPAGATION_NEVER表示当前方法不应该运行在事务上下文中,如果当前正有一个事务在运行则会抛出异常。
PROPAGATION_NOT_SUPPORTED表示该方法不应该运行在事务中。如果存在当前事务,在该方法运行期间,当前事务将被挂起。如果使用JTATransactionManager的话,则需要访问TransactionManager。
PROPAGATION_REQUIRED表示当前方法必须运行在事务中。如果当前事务存在,方法将会在该事务中运行。否则,将会启动一个新的事务。
PROPAGATION_REQUIRES_NEW表示当前方法必须运行在它自己的事务中。一个新的事务将会被启动。如果存在当前事务,在该方法执行期间,当前事务会被挂起。如果使用JTATransactionManager,则需要访问TransactionManager
PROPAGATION_SUPPORTS表示当前方法不需要事务上下文,但是如果存在当前事务的话,那么该方法会在这个事务中运行。

    这些传播行为分别对应了EJB的容器管理事务(CMT)所支持的传播规则。例如,Spring的 PROPAGATION_REQUIRES_NEW 等同于CMT的RequiresNew。Spring还添加了一个CMT中没有的传播行为:PROPAGATION_NESTED以支持嵌套式事务。
    传播规则回答了这样一个问题,即新的事务应该被启动还是被挂起,或者方法是否要在事务环境中运行。
    例如,如果一个方法声明了PROPAGATION_REQUIRES_NEW行为,这意味着事务边界与方法自己的边界是一样的:方法在开始执行的时候启动一个新事务并在方法返回或者抛出异常时结束该事务。如果方法具有PROPAGATION_REQUIRED的行为,事务的边界取决于是否已经有事务正在执行。
(2) 事务隔离级别:
    声明式事务的第二个维度就是隔离级别(isolation level)。隔离级别定义了一个事务可能受其他并发事务影响的程度。另一种考虑隔离级别的方式就是将其想象成事务对于事务性数据的自私程度。
    在典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自任务。并发,虽然是必须的,但可能会导致以下问题:

  • 脏读(Dirty reads) --- 脏读发生在一个事务读取了另一个事务修改了但是尚未提交的数据时。如果修改在稍后被回滚了,那么第一个事务所获取到的数据就是无效的。
  • 不可重复读(Nonrepeatable read) --- 不可重复读发生在一个事务执行相同的查询两次或两次以上,但是每次都得到不同的数据时。这通常是因为另一个并发事务在两次查询期间执行了数据更新操作。
  • 幻读(Phantom read) --- 幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录。

    在理想状态下,事务之间将完全隔离,从而可以防止这些问题发生。然而,完全隔离会影响性能,因为隔离经常涉及到锁定在数据库中的记录(甚至有时是锁表)。完全隔离要求事务相互等待来完成工作,会阻碍并发。因此,可以根据业务场景选择不同的隔离级别。

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

ISOLATION_READ_UNCOMMITTED是最高效的事务隔离级别,但是事务隔离的程度最低,事务可能会导致脏读、不可重复读、幻读。另一个极端则是ISOLATION_SERIALIZABLE,它能阻止所有的隔离问题,但效率是最低的。

(3) 只读:
    声明式事务的第三个特性是它是否为只读事务。如果事务只对后端的数据库进行读操作,数据库可以利用事务的只读特性来进行一些特定的优化。通过将事务设置为只读,就可以给数据库一个机会,让它用它认为盒实的优化措施。

    因为只读优化是在事务启动的时候由数据库实施的,只有对那些具备启动一个新事务的传播行为(PROPAGATION_REQUIRED、PROPAGATION_REQUIRES_NEW、PROPAGATION_NESTED)的方法来说,将事务声明为只读才有意义。
    另外,如果采用Hibernate作为持久化机制,那么将事务声明为只读会导致Hibernate的flush模式被设置为 FLUSH_NEVER。这回告诉Hibernate避免和数据库进行不必要的对象同步,并将所有的更新延迟到事务结束。
(4) 事务超时:
    为了使应用程序很好地运行,事务不能运行太长的时间。因此,声明式事务下一个特性就是超时(timeout)。
    由于事务可能涉及对后端数据库的锁定,所以如果事务的运行时间变得特别长,那么将会占用数据库资源。可以声明一个事务,在指定的秒数后自动回滚而不是等待其结束。
    因为超时时钟会在事务开始时启动,所以,只有那些具备可能启动一个新事务的传播行为(PROPAGATION_REQUIRED、PROPAGATION_REQUIRES_NEW、PROPAGATION_NESTED)的方法来所,声明事务超时才有意义。
(5) 事务回滚规则:
    事务的回滚规则定义了哪些异常会导致事务回滚,哪些异常不会回滚;默认情况下,事务只有在遇到运行时异常时才会回滚,而在遇到检查异常时不会回滚(这一行为与EJB的回滚行为是一致的)。
    但是可以声明事务在遇到特定的检查异常时,像遇到运行时异常那样进行回滚。同样,还可以声明事务遇到特定的异常不回滚,即使这些异常时运行时异常。

七、Spring声明式事务配置示例
    事务配置中可配置属性的简单使用:
    1. 事务的传播行:
        @Transactional(propagation=Propagation.REQUIRED)
    2. 事务的隔离级别:
        @Transactional(isolation=Isolation.READ_UNCOMMITTED) //读取未提交数据会出现脏读、不可重复读,基本不使用    
    3. 只读:
        @Transactional(readOnly=true)
        该属性用于设置读当前事务是否为只读事务,设置为true表示只读,false则表示可读写,默认值为false。
    4. 事务的超时性:
        @Transactional(timeout=30)
    5. 事务回滚:
        指定单一异常类:@Transactional(rollbackFor=RuntimeException.class)
        指定多个异常类:@Transactional(rollbackFor={RuntimeException.class, Exception.class})
        注释:该属性用于设置需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,则进行事务回滚。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值