Spring-jdbc:事务管理器的使用

事务的概念

首先要明确一下事务的概念:

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

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

事务有四个关键属性:

1、原子性(atomicity):事务是一个原子操作,由一系列动作组成。事务的原子性确保这些动作要么全部完成要么一个也不起作用。

2、一致性(consistency):一旦事务中所有动作完成,事务就会被提交,数据和资源就处于一种满足业务规则的一致性状态中。事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。因此当数据库只包含成功事务提交的结果时,就说数据库处于一致性状态。如果数据库系统 运行中发生故障,有些事务尚未完成就被迫中断,这些未完成事务对数据库所做的修改有一部分已写入物理数据库,这时数据库就处于一种不正确的状态,或者说是 不一致的状态。

3、隔离性(isolation):一个事务的执行不能其它事务干扰。即一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰。

4、持久性(durability):也称永久性,指一个事务一旦提交,它对数据库中的数据的改变就应该是永久性的。接下来的其它操作或故障不应该对其执行结果有任何影响。

任何一个事务都必须满足以上四个特性。

Spring 在不同的事务管理 API 之上定义了一个抽象层. 而应用程序开发人员不必了解底层的事务管理 API, 就可以使用 Spring 的事务管理机制.
Spring 既支持编程式事务管理, 也支持声明式的事务管理.
编程式事务管理: 将事务管理代码嵌入到业务方法中来控制事务的提交和回滚. 在编程式管理事务时, 必须在每个事务操作中包含额外的事务管理代码.
声明式事务管理: 大多数情况下比编程式事务管理更好用. 它将事务管理代码从业务方法中分离出来, 以声明的方式来实现事务管理. 事务管理作为一种横切关注点, 可以通过 AOP 方法模块化. Spring 通过 Spring AOP 框架支持声明式事务管理.

Spring中的事务管理

简单的来说:

对于纯JDBC操作数据库,想要用到事务,可以按照以下步骤进行:

获取连接 Connection con = DriverManager.getConnection()
开启事务con.setAutoCommit(true/false);
执行CRUD
提交事务/回滚事务 con.commit() / con.rollback();
关闭连接 conn.close();

使用Spring的事务管理功能后,我们可以不再写步骤 2 和 4 的代码,而是由Spirng 自动完成。那么Spring是如何在我们书写的 CRUD 之前和之后开启事务和关闭事务的呢?解决这个问题,也就可以从整体上理解Spring的事务管理实现原理了。下面简单地介绍下,注解方式为例子

配置文件开启注解驱动,在相关的类和方法上通过注解@Transactional标识。
spring 在启动的时候会去解析生成相关的bean,这时候会查看拥有相关注解的类和方法,并且为这些类和方法生成代理,并根据@Transaction的相关参数进行相关配置注入,这样就在代理中为我们把相关的事务处理掉了(开启正常提交事务,异常回滚事务)。
真正的数据库层的事务提交和回滚是通过binlog或者redo log实现的。

事务的传播属性

另一个需要注意的问题是,如何处理事务和事务之间的关系。一个典型的例子是当事务方法被另一个事务方法调用时, 必须指定事务应该如何传播(通过propagation属性定义)。比如: 方法可能继续在现有事务中运行, 也可能开启一个新事务, 并在自己的事务中运行.事务的传播行为可以由传播属性指定. Spring 定义了 7 种类传播行为.
Spring-jdbc:事务管理器的使用_第1张图片

前两种是使用最多的属性值。

对前两种属性的说明:

REQUIRED 传播行为:

下图是一个purchase() 事务方法被另一个事务方法 checkout() 调用时, 它默认会在现有的事务内运行. 这个默认的传播行为就是 REQUIRED. 因此在 checkout() 方法的开始和终止边界内只有一个事务. 这个事务只在 checkout() 方法结束的时候被提交。也就意味着如果有一个purchase() 没有成功执行,那么另一个做的更改也不会被提交。

Spring-jdbc:事务管理器的使用_第2张图片

REQUIRES_NEW 传播行为:

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


Spring-jdbc:事务管理器的使用_第3张图片

执行顺序为:Tx1开始->Tx1挂起->Tx2开始->Tx2结束->Tx1继续->Tx1挂起->Tx3开始->Tx3结束->Tx1继续->Tx1结束


注意:这些属性指定是针对被调用的事务方法的。


接下来我们通过分析一些嵌套事务的场景,来深入理解spring事务传播的机制。

假设外层事务 Service A 的 Method A() 调用 内层Service B 的 Method B()

PROPAGATION_REQUIRED(spring 默认)


如果ServiceB.methodB() 的事务级别定义为 PROPAGATION_REQUIRED,那么执行 ServiceA.methodA() 的时候spring已经起了事务,这时调用 ServiceB.methodB(),ServiceB.methodB() 看到自己已经运行在 ServiceA.methodA() 的事务内部,就不再起新的事务。

假如 ServiceB.methodB() 运行的时候发现自己没有在事务中,他就会为自己分配一个事务。

这样,在 ServiceA.methodA() 或者在 ServiceB.methodB() 内的任何地方出现异常,事务都会被回滚。

PROPAGATION_REQUIRES_NEW

比如我们设计 ServiceA.methodA() 的事务级别为 PROPAGATION_REQUIRED,ServiceB.methodB() 的事务级别为 PROPAGATION_REQUIRES_NEW。

那么当执行到 ServiceB.methodB() 的时候,ServiceA.methodA() 所在的事务就会挂起,ServiceB.methodB() 会起一个新的事务,等待 ServiceB.methodB() 的事务完成以后,它才继续执行。

他与 PROPAGATION_REQUIRED 的事务区别在于事务的回滚程度了。因为 ServiceB.methodB() 是新起一个事务,那么就是存在两个不同的事务。如果 ServiceB.methodB() 已经提交,那么 ServiceA.methodA() 失败回滚,ServiceB.methodB() 是不会回滚的。如果 ServiceB.methodB() 失败回滚,如果他抛出的异常被 ServiceA.methodA() 捕获,ServiceA.methodA() 事务仍然可能提交(主要看B抛出的异常是不是A会回滚的异常)。

PROPAGATION_SUPPORTS


假设ServiceB.methodB() 的事务级别为 PROPAGATION_SUPPORTS,那么当执行到ServiceB.methodB()时,如果发现ServiceA.methodA()已经开启了一个事务,则加入当前的事务,如果发现ServiceA.methodA()没有开启事务,则自己也不开启事务。这种时候,内部方法的事务性完全依赖于最外层的事务。

PROPAGATION_NESTED

现在的情况就变得比较复杂了, ServiceB.methodB() 的事务属性被配置为 PROPAGATION_NESTED, 此时两者之间又将如何协作呢?

ServiceB#methodB 如果 rollback, 那么内部事务(即 ServiceB#methodB) 将回滚到它执行前的 SavePoint 而外部事务(即 ServiceA#methodA) 可以有以下两种处理方式:

a、捕获异常,执行异常分支逻辑

void methodA() {    
        try {    
            ServiceB.methodB();    
        } catch (SomeException) {    
            // 执行其他业务, 如 ServiceC.methodC();    
        }    
    }

这种方式也是嵌套事务最有价值的地方, 它起到了分支执行的效果, 如果 ServiceB.methodB 失败, 那么执行 ServiceC.methodC(), 而 ServiceB.methodB 已经回滚到它执行之前的 SavePoint, 所以不会产生脏数据(相当于此方法从未执行过), 这种特性可以用在某些特殊的业务中, 而 PROPAGATION_REQUIRED 和 PROPAGATION_REQUIRES_NEW 都没有办法做到这一点。

b、 外部事务回滚/提交 代码不做任何修改, 那么如果内部事务(ServiceB#methodB) rollback, 那么首先 ServiceB.methodB 回滚到它执行之前的 SavePoint(在任何情况下都会如此), 外部事务(即 ServiceA#methodA) 将根据具体的配置决定自己是 commit 还是 rollback

数据库的隔离级别

当同一个应用程序或者不同应用程序中的多个事务在同一个数据集上并发执行时, 可能会出现许多意外的问题
并发事务所导致的问题可以分为下面三种类型:

脏读:一事务对数据进行了增删改,但未提交,另一事务可以读取到未提交的数据。如果第一个事务这时候回滚了,那么第二个事务就读到了脏数据。

不可重复读:一个事务中发生了两次读操作,第一次读操作和第二次操作之间,另外一个事务对数据进行了修改,这时候两次读取的数据是不一致的。

幻读:第一个事务对一定范围的数据进行批量修改,第二个事务在这个范围增加一条数据,这时候第一个事务就会丢失对新增数据的修改。

从理论上来说, 事务应该彼此完全隔离, 以避免并发事务所导致的问题. 然而, 那样会对性能产生极大的影响, 因为事务必须按顺序运行。在实际开发中, 为了提升性能, 事务会以较低的隔离级别运行。事务的隔离级别可以通过隔离事务属性指定(通过isolation属性指定)。

Spring-jdbc:事务管理器的使用_第4张图片
下面是描述:
Spring-jdbc:事务管理器的使用_第5张图片
事务的隔离级别要得到底层数据库引擎的支持, 而不是应用程序或者框架的支持.
Oracle 支持的 2 种事务隔离级别:READ_COMMITED , SERIALIZABLE
Mysql 支持 4 中事务隔离级别.
隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。
大多数的数据库默认隔离级别为 Read Commited,比如 SqlServer、Oracle
少数数据库默认隔离级别为:Repeatable Read 比如: MySQL InnoDB

举个栗子

下面以一个银行转账的例子说明在Spring中如何通过注解的方式使用事务管理器:

文件布局如下:

Spring-jdbc:事务管理器的使用_第6张图片

要使用Spring的事务管理器首先需要在xml文件中进行配置

1、扫描包、导入资源文件、配置c3p0数据源、配置jdbcTemplate(这些配置见:http://blog.csdn.net/u013468917/article/details/52217954)

2、配置事务管理器:

	
	
		
	

3、启用事务注解(需要启用tx命名空间)

	
	

数据库很简单:

Spring-jdbc:事务管理器的使用_第7张图片

每人的账户上都有2000余额。

定义数据库操作接口和实现类:

package spring.tx;

public interface BankAccountDAO {
	/**
	 * 用于更新账户余额:加操作
	 * @param userId 待更新账户的的用户ID
	 * @param amount 增加数额
	 */
	public void updateUserAccountAdd(int userId, double amount);
	
	/**
	 * 用于减少账户余额
	 * @param userId
	 * @param amount
	 */
	public void updateUserAccountDec(int userId, double amount);
}

实现类:

package spring.tx;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

@Repository("bankAccountDAO")
public class BankAccountDAOImp implements BankAccountDAO{
	
	@Autowired
	private JdbcTemplate jdbcTemplate = null;

	@Override
	public void updateUserAccountAdd(int userId, double amount) {
		//目标账户余额加上转账数额
		String sql3 = "UPDATE bankaccount SET balance = balance + ? WHERE user_id = ?";
		jdbcTemplate.update(sql3, new Object[]{amount,userId});
	}

	@Override
	public void updateUserAccountDec(int userId, double amount) {
		
		//查询余额是否足够,如果余额不足则抛出异常
		String sql = "SELECT balance FROM bankaccount WHERE user_id = ? ";
		int  balance = jdbcTemplate.queryForObject(sql, Integer.class,userId );
		if (balance

可以看到这里我自定义了一个异常:userAccountException

package spring.tx;


public class UserAccountException extends RuntimeException {

	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;

	@Override
	public String getMessage() {
		// TODO Auto-generated method stub
		return super.getMessage();
	}

	public UserAccountException() {
		super();
		// TODO Auto-generated constructor stub
	}

	public UserAccountException(String message) {
		super(message);
		// TODO Auto-generated constructor stub
	}
}

下面是业务逻辑的接口和实现类:

package spring.tx;

public interface BankAccountService {
	/**
	 * 
	 * @param originalAccount 转账原账户
	 * @param dstAccount 转账目的账户
	 * @param amount 转账的数额
	 */
	public void accountTransfer(int originalAccount, int dstAccount,double amount);
}
package spring.tx;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Service("bankAccountService")
public class BankAccountServiceImp implements BankAccountService{
	@Autowired
	private BankAccountDAO bankAccountDao;

	//添加事务注解
	//1.使用 propagation 指定事务的传播行为, 即当前的事务方法被另外一个事务方法调用时
	//如何使用事务, 默认取值为 REQUIRED, 即使用调用方法的事务
	//REQUIRES_NEW: 事务自己的事务, 调用的事务方法的事务被挂起. 
	//2.使用 isolation 指定事务的隔离级别, 最常用的取值为 READ_COMMITTED
	//3.默认情况下 Spring 的声明式事务对所有的运行时异常进行回滚. 也可以通过对应的
	//属性进行设置. 通常情况下去默认值即可. 
	//4.使用 readOnly 指定事务是否为只读. 表示这个事务只读取数据但不更新数据, 
	//这样可以帮助数据库引擎优化事务. 若真的只是一个只读取数据库值的方法, 应设置 readOnly=true
	//5.使用 timeout 指定强制回滚之前事务可以占用的时间.  
	@Transactional(propagation=Propagation.REQUIRED,
			isolation=Isolation.READ_COMMITTED,
			readOnly=false,
			timeout=3
			)
	@Override
	public void accountTransfer(int originalAccount, int dstAccount,
			double amount) {		
		bankAccountDao.updateUserAccountAdd(dstAccount, amount);
		bankAccountDao.updateUserAccountDec(originalAccount, amount);
	}

}

注意timeOut属性的时间单位为 秒,因为减钱操作会抛出异常,这里没有处理,为了测试事务功能就把加钱操作放在前面了。


测试一下:

package spring.tx;

import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class TransanctionTest {
	
	private BankAccountService bankAccountService = null;
	private ApplicationContext ctx = null;
	{
		ctx = new ClassPathXmlApplicationContext("ApplicationContext.xml");
		bankAccountService = (BankAccountService) ctx.getBean("bankAccountService");
	}
	@Test
	public void testTransanction() {
		bankAccountService.accountTransfer(1, 2, 20000);
	}

}
这里账户1向账户2转账20000,很显然账户1没有这么多钱,所以账户1的减钱操作和账户2的加钱操作都不能被执行并抛出异常:

Spring-jdbc:事务管理器的使用_第8张图片

数据库中的金钱数额也没有发生变化。



你可能感兴趣的:(javaEE-SSM)