浅谈 Spring 事务管理

来源:互联网 发布:淘宝详情页怎么做美观 编辑:程序博客网 时间:2024/06/06 03:25

事务基本原理

事务是由一个或者多个活动所组成的工作单元,它是并发控制的基本单位。在软件开发领域,人们使用 ACID 来描述事务的 4 个特性。

  • 原子性(atomicity):事务具有原子性,它是业务中的基本工作单元。原子性确保事务中的操作要么全部发生,要么全部不发生。
  • 一致性(consistency):一个事务无论失败与否,都需要确保系统数据是保持一致的,而不能出现前后不一致的情况。
  • 隔离性(isolation):事务之间是相互隔离的。隔离性确保其他事务对数据库的操作不会影响到当前事务。需要注意的是,隔离性有级别之分,具体可见下文的隔离性研究。
  • 持久性(durability):一旦事务完成,需要保证事务对数据库的操作持久化。

在 JDBC 中,默认以非事务的形式执行 SQL 语句,如果想开启事务,需要以下步骤:

// 开启事务Connection.setAutoCommit(false);// 提交事务Connection.commit();// 若发生异常,则回滚// 回滚可以全部回滚,或从指定断点回滚Connection.rollback(Savepoint sp);

显然,如果每次我们为了实现事务,而把业务代码包在这些 JDBC 的代码中,那无疑是非常糟糕的。幸好 Spring 为我们解决了这些难题,不过在此之前,先让我们了解下隔离性。


事务隔离级别

在事务的 4 个特性中,隔离性的理解难度是最高的,因此我觉得,隔离性确实有必要单独拿出来聊一聊。

因为事务经常是并发执行的,所以会因隔离性级别的不同而碰到脏读、不可重复读、幻读的问题。

  • 脏读:
    一个事务读取另一个事务尚未提交的更新。例如:张三在事务T1转账100元,同时在事务T2存入200元,若此时事务T2在事务T1还没提交前就读取事务T1的处理数据,有可能因为事务T1发生异常回滚但是事务T2还是读取了回滚前的数据。
  • 不可重复读:
    同一个事务中,两次读取相同的记录,结果不一样。例如事务T1读取某一数据,事务T2读取并修改了该数据,T1为了对读取值进行检验而再次读取该数据,却得到了不同的结果。
  • 幻读:
    两次读取的结果包含的行记录不一样。 例如事务T1读取了几行数据,然后另一事务T2插入一些数据,在之后的查询中,事务T1就会发现两次读取的行数不一致。

以下是数据库的 4 种隔离级别及对应场景。

隔离级别 对应场景 读未提交(read uncommitted) 允许脏读 读提交(read committed) 避免脏读,允许不可重复读和幻读 重复读(repeatable read) 避免脏读、不可重复读,允许幻读 串行化(serializable) 串行化,事务只能依次执行,避免脏读、不可重复读、幻读


事务隔离级别越高,数据库性能越差。读未提交(read uncommitted)是效率最高的隔离级别,但它的隔离程度是最低的。相反,串行化(serializable)是隔离程度最好的隔离级别,也是能保证事务完全隔离的级别,可是完全的隔离会极大地阻碍数据库的性能。在 MySQL 中,默认事务隔离级别为 repeatable read。


Spring 的事务支持

有了以上对事务基本原理的介绍作基础,接下来我们就可以开始研究 Spring 是如何实现事务的。

实际上,Spring 本身并不直接管理事务,而是将其交给了事务管理器。

image

这些事务管理器,会将事务管理的职责委托给特定平台的事务实现,而作为开发者,我们无需关注底层的事务实现是什么。

在 Spring 中,提供了两种支持事务的方式。一是编程式事务,二是声明式事务。那么,两者的区别是什么呢?

编程式事务允许开发人员在代码中精确定义事务的边界。 这是因为,使用编程式事务,开发人员可以随心所欲地根据自己的需求,在自己需要的地方加入事务控制。但是,这也带来了不好的地方。那就是实现编程式事务的代码会在一定的程度上侵略你自身的代码。

声明式事务允许开发人员通过 XML 配置文件或注解的方式实现事务的控制,这主要是通过 Spring AOP 框架实现的。 但相对编程式事务,声明式事务在细粒度的控制上比较薄弱。它只能在类级别或者方法级别实现事务控制。不过,在易用性上,声明式事务又是远胜于编程式事务的。

至于究竟是使用编程式事务还是声明式事务,还是得根据实际业务所需的细粒度和易用性来权衡。

不过,由于我个人偏好使用声明式事务,因此在本文接下来的内容中,也都是基于声明式事务实现的


声明式事务

我们上面说过,Spring 的声明式事务支持,有两种实现方法,一是通过 XML 配置文件,二是通过注解。

而无论是使用 XML 或是注解方式声明事务,都需要在配置文件中声明事务管理器。

<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">    <property name="dataSource" ref="dataSource" /></bean>

这里我们以支持 JDBC 和 iBatis 的 DataSourceTransactionManager 事务管理器为例。我们注意到,在事务管理器中,需要一个 id 为 dataSource 的 bean。实际上,这个 bean 是用于配置我们的数据源的。

接下来,我们再来瞅瞅声明式事务的两种具体实现方式。

1. 使用 XML 方式:

首先,我们定义一个事务通知 txAdvice,其中的 <tx:method> 为方法定义了事务策略。

<tx:advice id="txAdvice" transaction-manager="txManager">    <tx:attributes>        <tx:method name="transfer*" propagation="REQUIRED"/>    </tx:attributes></tx:advice>

但是,txAdvice 只是定义了 AOP 通知,用于把事务的边界通知给方法,并不是完整的切面。这时候,我们还需要一个切点,来实现这件事。

<aop:config>    <aop:pointcut id="daoOperation" expression=        "execution(* springTransaction.AccountDao.*(..))"/>    <aop:advisor advice-ref="txAdvice" pointcut-ref="daoOperation"/></aop:config>

其中,pointcut 指明了切入点,advisor 指定了事务通知 txAdvice。

2. 使用注解方式:

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

嗯?什么?这就没了?就这一句?

对!你没看错!使用注解驱动事务,只需要在配置文件加上这句就足够了!它会告诉 Spring,在上下文中检查带有 @Transactional 注解的 bean,然后为它添加事务通知,就是这么简单!


事务属性

我相信,细心的同学会在思考,上述 <tx:method> 中的 propagation 究竟是个什么鬼。这就引出了我们最后的话题,事务属性。你一定很好奇,声明式事务,是如何为方法制定事务策略的。实际上,它是通过 5 大事务属性实现的,它们分别是:传播行为、隔离级别、是否只读、事务超时、回滚规则。

关于隔离级别与是否只读,我们不再讨论。

首先,是事务超时。熟悉数据库的同学应该都知道,事务实际上是通过持有后端数据库的锁实现的。为了不让锁资源因事务运行时间太长而被长期占用,我们可以设置一个超时时间,使事务在运行到指定时间后,会自动回滚,而不是占用数据库资源,影响数据库性能。

然后,是回滚规则。默认情况下,事务只有遇到运行时异常才会回滚,而遇到检查型异常时不会回滚。 但是,在实际业务中,这种默认的策略可能无法满足我们的需求。因此,Spring 提供了回滚机制供开发者选择。

最后,是传播行为,也是事务属性中最重要的一个环节。传播行为定义了事务的边界,它制定了何时创建事务或何时使用已有事务。让我们先看个让人头大的表格。

传播行为 含义 PROPAGATION_MANDATORY 函数必须在一个事务中执行,不存在事务则抛出异常 PROPAGATION_NEVER 函数不应该在事务中执行,若存在事务,则抛出异常 PROPAGATION_SUPPORTS 函数可以在事务中执行,如果不存在当前事,就以非事务方式执行。 PROPAGATION_NOT_SUPPORTED 函数不应该在事务中执行,若存在事务,在该函数运行期间,当前事务将被挂起 PROPAGATION_REQUIRED 函数必须在事务中执行,如果不存在当前事务,则启动新事务 PROPAGATION_NESTED 函数必须在事务中执行,如果不存在当前事务,则启动新事务,若存在当前事务,则函数在嵌套事务中运行。嵌套事务可以独立于当前事务进行单独的提交或回滚 PROPAGATION_REQUIRES_NEW 函数必须在新事务中执行,若存在当前事务,则当前事务将被挂起


在这里,我们主要介绍两种传播规则。我们假设有两个服务,外层服务 A 与内层服务 B。

PROPAGATION_REQUIRED(Spring 默认的传播规则)

如果内层服务 B 的事务级别定义为 PROPAGATION_REQUIRED,那么执行外层服务 A 的时候,由于已经起了事务,这时内层服务 B 看到自己已经运行在外层服务 A 的事务内部,就不再起新的事务。反之,若内层服务 B 运行的时候发现自己没有在事务中,它就会为自己分配一个新的事务。这样,在外层服务 A 或者内层服务 B 内的任何地方出现异常,都将导致事务的回滚。

PROPAGATION_NESTED

如果内层服务 B 的事务级别定义为 PROPAGATION_NESTED,那么情况就比较复杂了。若外层服务 A 不存在事务,那么此时 PROPAGATION_NESTED 与 PROPAGATION_REQUIRED 是一样的。但如果此时外层服务 A 存在事务,情况就不同了。
根据 PROPAGATION_NESTED 的名称,我们知道,内部服务 B 所在事务实际上是一个嵌套事务,它与外部事务是独立的。因此,它可以独立于外部事务进行单独的提交或回滚,而不受外部事务的影响,同样,如果它因异常发生了回滚,也不会影响到外部事务。更有趣的是,外部事务可以捕获到它的异常,从而有办法做出更多有趣的事情。

关于事务的学习,就到这边,欢迎各路大侠批评指正!

原创粉丝点击