9.Spring系列之事务

一、事务简介


事务管理是企业级应用程序开发中必不可少的技术,用来确保数据的完整性和一致性;

事务就是一系列的动作, 它们被当做一个单独的工作单元,这些动作要么全部完成,要么全部不起作用;

事务的四个关键属性(ACID):

  • 原子性(atomicity):事务是一个原子操作,由一系列动作组成,事务的原子性确保动作要么全部完成要么完全不起作用;
  • 一致性(consistency):一旦所有事务动作完成,事务就被提交,数据和资源就处于一种满足业务规则的一致性状态中;
  • 隔离性(isolation):可能有许多事务会同时处理相同的数据,因此每个事物都应该与其他事务隔离开来,防止数据损坏;
  • 持久性(durability):一旦事务完成, 无论发生什么系统错误,它的结果都不应该受到影响,通常情况下,事务的结果被写到持久化存储器中

二、原始使用事务的问题


 在我们学习JDBC的时候,最常见的实现业务的写法是:

Connection conn = null;
try {
  conn = dataSource.getConnection();
  conn.setAutoCommit(false);
  /** 执行业务方法 **/
  conn.commit();
} catch (Exception e) {
  if(conn != null) {
    conn.rollback();
  }
} finally {
  if(conn != null) {
    conn.close();
  }
}

可以看出:

1.我们在写业务方法时,必须为不同的方法重写类似代码;

2.刚刚也说了,这是学JDBC时候经常用的写法来达到控制事务,那么如果我们还其它数据库存取方案,代码则需要大量做出修改。

三、Spring中的事务


作为企业级应用程序框架,Spring 在不同的事务管理 API 之上定义了一个抽象层,而应用程序开发人员不必了解底层的事务管理 API,就可以使用 Spring 的事务管理机制;

Spring 既支持编程式事务管理,也支持声明式的事务管理;

编程式事务管理:将事务管理代码嵌入到业务方法中来控制事务的提交和回滚,在编程式管理事务时,必须在每个事务操作中包含额外的事务管理代码;

声明式事务管理:大多数情况下比编程式事务管理更好用,它将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理,事务管理作为一种横切关注点,可以通过 AOP 方法模块化,Spring 通过 Spring AOP 框架支持声明式事务管理;

注意:声明式事务时基于AOP框架支持,所以我们大概能猜测得到,在执行业务方法之前,前置通知开启事务,执行业务方法之后后置通知提交事务,如果业务方法执行异常了那么异常通知回滚事务。

四、Spring中的事务管理器


Spring 从不同的事务管理 API 中抽象了一整套的事务机制,开发人员不必了解底层的事务 API,就可以利用这些事务机制,有了这些事务机制, 事务管理代码就能独立于特定的事务技术了;

Spring 的核心事务管理抽象是org.springframework.transaction Interface PlateformTransactionManager,它为事务管理封装了一组独立于技术的方法,无论使用 Spring 的哪种事务管理策略(编程式或声明式),事务管理器都是必须的 !

其不同实现为:

org.springframework.jdbc.datasource Class DataSourceTransaction
在应用程序中只需要处理一个数据源, 而且通过 JDBC 存取
org.springframework.transaction.jta Class JtaTransactionManager
在 JavaEE 应用服务器上用 JTA(Java Transaction API) 进行事务管理
org.springframework.orm.hibernate3 Class HibernateTransactionManager
用 Hibernate 框架存取数据库

注意:事务管理器以普通的 Bean 形式声明在 Spring IOC 容器中

五、使用注解的方式声明式的管理事务

1.为方法添加 @Transactional 注解

2.根据 Spring AOP 基于代理机制,只能标注公有方法

3.在方法或者类级别上添加 @Transactional 注解,如果添加到类上,这个类中的所有公共方法都会被定义成支持事务处理

4.配置如下:

<!--  配置 C3P0 数据源 --> 
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
  <property name="user" value="${jdbc.user}" /> 
  <property name="password" value="${jdbc.password}" /> 
  <property name="jdbcUrl" value="${jdbc.jdbcUrl}" /> 
  <property name="driverClass" value="${jdbc.driverClass}" /> 
  <property name="initialPoolSize" value="${jdbc.initPoolSize}" /> 
  <property name="maxPoolSize" value="${jdbc.maxPoolSize}" /> 
</bean>
<!--  配置事务管理器 --> 
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  <property name="dataSource" ref="dataSource" /> </bean> <!-- 启用事务注解 --> <tx:annotation-driven transaction-manager="transactionManager" />

注意:注解声明式事务是最常用的方式,建议使用注解声明式来控制事务,其它事务不再罗列。

六、事务的传播行为


当事务方法被另一个事务方法调用时, 必须指定事务应该如何传播.。比如方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行;

以下是Spring 支持的事务传播行为:

REQUIRED:调用方有事务在运行,被调用方的事务方法就在调用方的事务方法中运行;如果调用方没有事务,则新建事务在本身事务中运行;
REQUIRED_NEW:当前方法有事务在运行,被调用方的事务方法在它自己的事务内运行,调用方的事务方法被挂起,待被调用方的事务方法执行结束后继续;
SUPPORTS:支持当前事务,假设当前没有事务,就以非事务方式运行;
NOT_SUPPORTS:以非事务方式运行操作,假设调用方有事务在运行,就把当前事务挂起;
MANDATORY:调用方有事务在运行,被调用方的事务方法就在调用方的事务方法中运行;如果调用方没有事务,就抛出异常;
NEVER:以非事务方式运行,假设调用方有事务在运行,则抛出异常;
NESTED:如果调用方有事务方法在运行,被调用方应该在调用方事务的嵌套事务内运行,否则,就启动一个新的事务并在自己的事务内运行

注意:常用的是REQUIRED和REQUIRED_NEW,以下详细说明这两种事务传播行为 !

--->REQUIRED传播行为:以网上搜来的图为例来说明

说明:

1.在checkout这个方法内运行着事务,分别是TX1开始事务,TX1结束事务;

2.在本事务内,存在着被调用方,被调用方也运行着事务方法,从图中看,这可能是循环调用存在事务的purchase方法;

3.我们假设就是循环调用存在事务的purchase方法,那么以伪代码来体现:

@Transactional
public void checkout() {
  // 执行批量购买
   for(int i=0;i<10;i++) {
     purchase(i);
   }
}

@Transactional
public void purchase(i) {
  System.out.println("购买了i="+i);
}

4.进入checkout方法时开启事务,开始执行for循环时,purchase发现调用方方法checkout存在事务,则把自己也交给checkout事务管理;

5.一旦for循环执行purchase其中一个方法抛出异常,那么之前purchase执行成功都不算数,全部被回滚,这就体现了要么事务一起成功,要么一起失败。

说明:默认的事务时REQUIRED,即所有被调用的方法都是用调用方的事务,不再使用自身的事务。

效果:如果被调用方的方法一旦出现异常,那么所有被调用方执行的结果都被回滚。

--->REQUIRED_NEW传播行为:以网上搜来的图为例来说明

另一种常见的传播行为是 REQUIRES_NEW,它表示该方法必须启动一个新事务,并在自己的事务内运行,如果有事务在运行,就应该先挂起它

说明:

1.在checkout这个方法内运行着事务,分别是TX1开始事务,TX1结束事务;

2.在本事务内,存在着被调用方,被调用方也运行着事务方法,从图中看,这可能是循环调用存在事务的purchase方法;

3.我们假设就是循环调用存在事务的purchase方法,那么以伪代码来体现:

@Transactional
public void checkout() {
  // 执行批量购买
   for(int i=0;i<10;i++) {
     purchase(i);
   }
}

@Transactional
public void purchase(i) {
  System.out.println("购买了i="+i);
}

4.进入checkout方法时开启事务,开始执行for循环时,purchase发现调用方方法checkout存在事务,则把checkout方法的事务挂起,开始自己的事务

5.假设当i=5的时候,执行抛出了异常,那么从i=0到i=4的这5次执行并不会被回滚,因为它是单独的一个事务,而i=5这次执行被回滚了。

效果:如果被调用方的方法出现异常,那么所有被调用方执行的成功的结果不会被回滚。

七、高并发中的事务问题


1.更新丢失

两个事务同时更新一行数据,最后一个事务的更新会覆盖掉第一个事务的更新,从而导致第一个事务更新的数据丢失,这是由于没有加锁造成的

第一类更新丢失(回滚覆盖):撤消一个事务时,在该事务内的写操作要回滚,把其它已提交的事务写入的数据覆盖了;

第二类更新丢失(提交覆盖):提交一个事务时,写操作依赖于事务内读到的数据,读发生在其他事务提交前,写发生在其他事务提交后,把其他已提交的事务写入的数据覆盖了,这是不可重复读的特例。

2.脏读

一个事务看到了另一个事务未提交的更新数据;当事务读取尚未提交的数据时,就会发生这种情况。

3.不可重复读

一个事务中两次读同一行数据,可是这两次读到的数据不一样。

4.幻读

一个事务中两次查询,但第二次查询比第一次查询多了或少了几行或几列数据。

八、事务的隔离级别


从理论上来说,事务应该彼此完全隔离,以避免并发事务所导致的问题(就如以上问题)。然而,那样会对性能产生极大的影响,因为事务必须按顺序运行;

在实际开发中,为了提升性能, 事务会以较低的隔离级别运行;

Spring支持的事务隔离级别:

待续...

原文地址:https://www.cnblogs.com/Json1208/p/8763367.html