【Java EE】深入Spring数据库事务管理

深入Spring数据库事务管理

数据库事务是企业应用最为重要的内容之一。首先讨论Spring数据库的事务应用,然后讨论Spring中最著名的注解之一——@Transactional。

Spring数据库事务管理器的设计

在Spring中数据库事务是通过PlatformTransactionManager进行管理的,而能够支持事务的是org.springframework.transaction.support.TransactionTemplate模板,它是Spring所提供的事务管理器的模板,源码如下:

//事务管理器
private PlatformTransactionManager transactionManager;
......
@Override
public <T> T execute(TransactionCallback<T> action) throws TransactionException{
	// 使用自定义的事务管理器
	if(this.transactionManager instanceof CallbackPreferringPlatformTransactionManager){
		return ((CallbackPreferringPlatformTransactionManager)this.transactionManager).execute(this,action);
	}else{//系统默认管理器
		//获取事务状态
		TransactionStatus status = this.transactionManager.getTransaction(this);
		T result;
		try{
			//回调接口方法
			result = action.doInTransaction(status);
		}catch(RuntimeException ex){
			//Transactional code threw application exception -> rollback
			//回滚异常方法
			rollbackOnException(status, ex);
			//抛出异常
			throw ex;
		}catch(Error ex){
			//Transactional code threw error -> rollback
			//回滚异常方法
			rollbackOnException(status, err);
			//抛出错误
			throw err;
		}catch(Throwable ex){
			//Transactional code threw unexpected exception -> rollback
			//回滚异常方法
			rollbackOnException(status, ex);
			//抛出无法捕获异常
			throw new UndeclaredThrowableException(ex, "TransactionCallback threw undeclared checked exception");
		}
		//提交事务
		this.transactionManager.commit(status);
		//返回结果
		return result;
	}
}

通过上面的源码,可以看到:

  • 事务的创建、提交和回滚是通过PlatformTransactionManager接口来完成的。
  • 当事务产生异常时会回滚事务,在默认的实现中所有的异常都会回滚。可以通过配置去修改在某些异常发生时回滚或者不会滚事务。
  • 当无异常时,会提交事务。
    在Spring中,有多种事务管理器,它们的设计如下:
    Spring事务管理器的设计
    其中可以看到多个数据库事务管理器,并且支持JTA事务,常用的是DataSourceTransactionManager,它集成抽象事务管理器AbstractPlatformTransactionManager,而AbstractPlatformTransactionManager又实现了PlatformTransactionManager。这样Spring就可以如同源码中那样使用PlatformTransactionManager接口的方法,创建、提交或者回滚事务了。
    PlatformTransactionManager接口的源码如下:
public interface PlatformTransactionManager{
 //获取事务状态
 TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
 // 提交事务
 void commit(TransactionStatus status) throws TransactionException;
 //回滚事务
 void rollback(TransactionStatus status) throws TransactionException;
}

配置事务管理器

对于MyBatis框架,用得最多的事务管理器是DataSourceTransactionManager(org.springframework.jdbc.datasource.DataSourceTransactionManager);如果是Hibernate框架,那么要用到spring-orm包org.springframework.orm.hibernate4.HibernateTransactionManager了。它们大同小异,一般而言,在使用时,还会加入XML的事务命名空间。如下所示:

<?xml version='1.0' encoding='UTF-8' ?>
<!-- was: <?xml version="1.0" encoding="UTF-8"?> -->
<beans xmlns="http://www.springframework.org/schema/beans"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
   xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
   xmlns:context="http://www.springframework.org/schema/context"
   xsi:schemaLocation="http://www.springframework.org/schema/beans 
      http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
       http://www.springframework.org/schema/aop 
       http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
       http://www.springframework.org/schema/tx 
       http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
       http://www.springframework.org/schema/context 
       http://www.springframework.org/schema/context/spring-context-4.0.xsd">
<!-- 数据库连接池 -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
   <property name="driverClassName" value="com.mysql.jdbc.Driver" />
   <property name="url" value="jdbc:mysql://localhost:3306/chapter13"/>
   <property name="username" value="root" />
   <property name="password" value="123456" />
   <property name="maxActive" value="255" />
   <property name="maxIdle" value="5" />
   <property name="maxWait" value="10000" />
</bean>
<!-- 事务管理器配置数据源事务 -->
<bean id="transactionManager"
   class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
   <property name="dataSource" ref="dataSource" />
</bean>
</beans>

这里先引入了XML的命名空间,然后定义了数据库连接池,于是使用了DataSourceTransactionManager去定义数据库事务管理器,并且注入了数据库连接池。这样Spring就知道已经将数据库事务委托给事务管理器transactionManager管理了。
在Spring中可以使用声明式事务或者编程式事务,编程式事务几乎不用,因此主要介绍声明式事务。声明式事务又可以分为XML配置和注解事务,但是XML方式也已经不用了。目前主流方法是注解@Transactional。

使用Java配置方式实现Spring数据库事务

用Java配置的方式来实现Spring数据库事务,需要在配置类中实现接口TransactionManagerConfigurer的annotationDrivenTransactionManager方法。Spring会把annotationDrivenTransactionManager方式返回的事务管理器作为程序中的事务管理器。下面是使用Java配置方式实现Spring的数据库事务配置代码:

/********* import ************/
@Configuration
@ComponentScan("com.ssm.chapter13.*")
//使用事务驱动管理器
@EnableTransactionManagement
public class JavaConfig implements TransactionManagerConfigurer{
	//数据源
	private DataSource dataSource = null;
	/**
	*配置数据源
	*@return 数据源.
	*/
	@Bean(name="dataSource")
	public DataSource initDataSource(){
		if(dataSource != null){
			return dataSource;
		}
		Properties props = new Properties();
		props.setProperty("driverClassName","com.mysql.jdbc.Driver");
		props.setProperty("url","jdbc:mysql://lcalhost:3306/chapter15");
		props.setProperty("username","root");
		props.setProperty("password","123456");
		props.setProperty("maxActiv","200");
		props.setProperty("maxIdle","20");
		props.setProperty("maxWait","30000");
		try{
			dataSource = BasicDataSourceFactory.createDataSource(props);
		}catch(Exception e){
			e.printStackTrace();
		}
		return dataSource;
	}
	/**
	*配置jdbcTemplate
	*@return jdbcTemplate
	*/
	@Bean(name="jdbcTemplate")
	public JdbcTemplate initjdbcTemplate(){
		JdbcTemplate jdbcTemplate = new JdbcTemplate();
		jdbcTemlate.setDataSource(initDataSource());
		return jdbcTemplate;
	}
	/**
	*实现接口方法,使得返回数据库事务管理器
	*/
	@Override
	@Bean(name = "transactionManager")
	public PlatformTransactionManager annotationDriverTransactionManager(){
		DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
		//设置事务管理器管理的数据源
		transactionManager.setDataSource(initDataSource());
		return transactionManager;
	}
}

注意,使用注解@EnableTransactionManagement后,在Spring上下文中使用事务注解@Transactional,Spring就会知道使用这个数据库事务管理器管理事务了。

编程式事务

编程式事务以代码的方式管理事务,需要使用一个事务定义接口——TransactionDefinition。只要使用默认的实现类——DefaultTransactionDefinition就可以了。示例代码如下:

ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-cfg.xml");
JdbcTemplate jdbcTemplate = ctx.getBean(JdbcTemplate.clsss);
//事务定义类
TransactionDefinition def = new DefaultTransactionDefinition();
PlatformTransactionManager transactionManager = ctx.getBean(PlatformTransactionManager.class);
TransactionStatus status = transactionManager.getTransaction(def);
try{
	//执行SQL语句
	jdbcTemplate.update("insert into t_role(role_name, note) values('role_name_transactionManagert','note_transactionManager')");
	//提交事务
	transactionManager.commit(status);
	//回滚事务
	transactionManager.rollback(status);
}

所有的事务都是由开发者自己进行控制的,由于事务已交由事务管理器管理,所以JbdcTemplate本身的数据库资源已经由事务管理器管理,因此当它执行完insert语句时不会自动提交事务,这个时候需要使用事务管理器的commit方法,回滚事务需要使用rollback方法。

声明式事务

声明式事务是一种约定型的事务,在大部分情况下,当使用数据库事务时,大部分的场景是在代码中发生了异常时,需要回滚事务,而不发生异常时则提交事务,从而保证数据库数据的一致性。
首先声明式事务允许自定义事务接口——TransactionDefinition,它可以由XML或者注解@Transactional进行配置。

Transactional的配置项

Transactional的源码如下:

package org.springframework.transaction.annotation;

import java.lang.annotation.Documented;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @Interface Transactional{
	@AliasFor("transactionManager");
	String value() default "";

	@AliasFor("value")
	String transactionManager() default "";

	Propagation propagation() default Propagation.REQUIRED;

	Isolation isolation() default Isolation.DEFAULT;

	int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;

	boolean readOnly() default false;
	
	Class<?extends Throwable>[] rollbackFor() default{};
	
	String[] rollbackForClassName() default{};
	
	Class<? extends Throwable>[] noRollbackFor() default{};

	String[] noRollbackForClassName() default {};
}

下面是它的配置项:

配置项 含义 备注
value 定义事务管理器 它是Spring IoC容器里的一个Bean id,这个Bean需要实现接口PlatformTransactionManager
transactionManager 同上 同上
isolation 隔离级别 这是一个数据库在多个事务同时存在时的概念,默认值取数据库默认隔离级别
propagation 传播行为 传播行为是方法之间调用的问题,默认值为Propagation.REQUIRED
timeout 超时时间 单位为秒,当超时时,会引发异常,默认会导致事务回滚
readOnly 是否开启只读事务 默认值为false
rollbackFor 回滚事务的异常类定义 也就是只有当方法产生所定义异常时,才回滚事务,否则就提交事务
rollbackForClassName 回复事务的异常类名定义 同rollbackFor,只是使用类名称定义
noRollbackFor 当产生哪些异常不回滚事务 当产生所定义异常时,Spring将继续提交事务
noRollbackForClassName 同noRollbackFor 同noRollbackFor,只是使用类的名称定义

isolation和propagation是最重要的内容。上述所有的属性将被会Spring放到事务定义类TransactionDefinition中,事务声明器的配置内容也是以这些为主了。
注意:使用声明式事务需要配置注解驱动,只需要在代码中加入如下配置即可使用@Transactional配置事务了:

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

使用XML进行配置事务管理器

介绍一种通用的XML声明式事务配置,它需要一个事务拦截器——TransactionInterceptor,可以把拦截器想象成AOP编程,首先配置它,代码如下:

<bean id="transactionInterceptor" class="org.springframework.transaction.interceptor.TransactionInterceptor">
	<property name="transactionManager" ref="transactionManager" />
	<!--配置事务属性-->
	<property name="transactionAttributes">
		<props>
		<!--key代表的是业务方法的正则式匹配,而其内容可以配置各类事务定义参数-->
			<prop key="insert*">PROPAGATION_REQUIRED,ISOLATION_READ_UNCOMMITTED</prop>
			<prop key="save*">PROPAGATION_REQUIRED, ISOLATION_READ_UNCOMMITTED</prop>
			<prop key="add*">PROPAGATION_REQUIRED,ISOLATION_READ_UNCOMMITTED</prop>
			<prop key="select*"PROPAGATION_REQUIRED,readOnly</prop>
			<prop key="get*">PROPAGATION_REQUIRED,readOnly</prop>
			<prop key="find*">PROPAGATION_REQUIRED,readOnly</prop>
			<prop key="del*">PROPAGATION_REQUIRED,ISOLATION_READ_UNCOMMITTED</prop>
			<prop key="remove*">PROPAGATION_REQUIRED,ISOLATION_READ_UNCOMMITTED</prop>
			<prop key="update*">PROPAGATION_REQUIRED,ISOLATION_READ_UNCOMMITTED</prop>
		</props>
	</property>
</bean>

配置transactionAttributes的内容是需要关注的重点,Spring IoC启动时会解析这些内容,放到事务定义类TransactionDefinition中,再运行时会根据正则式的匹配度决定方法采取哪种策略。这也揭示了声明式事务的底层原理——Spring AOP技术。
展示Spring方法采取的事务策略之外,还需要告诉Spring哪些类要使用事务拦截器进行拦截,为此再配置一个类BeanNameAutoProxyCreator:

<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">	
	<property name="beanNames">
		<list>
			<value>*ServiceImpl</value>
		</list>
	</property>
	<property name="interceptorNames">
		<list>
			<value>transactionInterceptor</value>
		</list>
	</property>
</bean>

BeanName属性告诉Spring如何拦截类。然后interceptorName则是定义事务拦截器,这样对应的类和方法就会被事务管理器所拦截了。

事务定义器

下面是事务定义器TransactionDefinition的源码;

package org.springframework.transaction;
import java.sql.Connection;
public interface TransactionDefinition{
	//传播行为常量定义(7个)
	int PROPAGATION_REQUIRED = 0; //默认传播行为
	int PROPAGATION_SUPPORTS = 1;
	int PROPAGATION_MANDATORY = 2;
	int PROPAGATION_REQUIRES_NEW = 3;
	int PROPAGATION_NOT_SUPPORTED = 4;
	int PROPATATION_NEVER = 5;
	int PROPATATION_NESTED = 6;
	//隔离级别定义(5个)
	int ISOLATION_DEFAULT = -1;//默认隔离级别
	//隔离级别定义(5个)
	int ISOLATION_READ_UNCOMMITTED = Connection.TRANSACTION_READ_UNCOMMITTED;
	int ISOLATION_READ_COMMITTED = Connection.TRANSACTION_REPEATABLE_READ;
	int ISOLATION_SERIALIZABLE = Connection.TRANSACTION_SERIALIZABLE;
	int TIMEOUT_DEFAULT = -1//-1代表永不超时
	//获取传播行为
	int getPropagationBehavior();
	//获取隔离级别
	int getIsolationLevel();
	//事务超时时间
	int getTimeout();
	//是否只读事务
	boolean isReadOnly();
	//获取事务定义器名称
	String getName();
}

以上就是关于事务定义器的内容,除了异常的定义,其他关于事务的定义都可以在这里完成。而对于事务的回滚内容,会以RollbackRuleAttribute和NoRollbackRuleAttribute两个类进行保存。

声明式事务的约定流程

约定十分重要,首先要理解@Transaction注解或者XML配置。@Transaction注解可以使用在方法或者类上面,在Spring IoC容器初始化时,Spring会读入这个注解或着XML配置的事务信息,并且保存到一个事务定义类里面(TransactionDefinition接口的子类),以备将来使用。当运行时会让Spring拦截注解标注的某一个方法或者类的所有方法,这就是AOP的原理。
首先Spring通过事务管理器(PlatformTransactionManager的子类)创建事务,与此同时会把事务定义中的隔离级别、超时时间等属性根据配置内容往事务上配置。然后启动开发者提供的业务代码,即Spring通过反射的方式调度开发者的业务代码,如果反射的结果是异常的,并且符合事务定义类回滚条件,Spring就会将数据库事务回滚,否则将数据库事务提交,这是Spring自己完成的。
声明式事务的流程如下:
声明式事务的流程图
以插入角色代码为例:

@Autowired
private RoleDao roleDao = null;

@Transactional(propagation=Propagation.REQUIRED,isolation=Isolation.DEFAULT,timeout=3)
public int insertRole(Role role){
	return roleDao.insert(role);
}

这里配置了Propagation.REQUIRED的传播行为,意味着当别的方法调度时,如果存在事务就沿用下来,如果不存在事务就开启新的事务,而隔离级别采用默认的,并且设置了超时时间为3秒。这就是Spring AOP技术的神奇之处,其底层的实现原理是动态代理,也就是只有代理对象相互调用才能像AOP那么神奇。

数据库的相关知识

数据库事务ACID特性

数据库事务正确执行的4个基础要素是原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。

  • 原子性:整个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节。事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务从来没有被执行过一样。
  • 一致性:指一个事务可以改变封装状态(除非它是一个只读的)。事务必须始终保持系统处于一致的状态,不管在任何给定的时间并发事务有多少。
  • 隔离性:它是指两个事务之间的隔离程度。
  • 持久性:在事务完成以后,该事务对数据库所做的更改便持久保存在数据库之中,并不会被回滚。

隔离性涉及多个事务并发的状态,会产生数据库丢失更新的问题,其次隔离性又分为多个层级。

丢失更新

在互联网中存在着抢购、秒杀等高并发场景、使得数据库在一个多事务的环境中运行,多个事务的并发会产生一系列的问题,主要的问题之一就是丢失更新,一般而言存在两类丢失更新。
第一类丢失更新:两个事务并发,其中一个回滚,另一个提交成功导致不一致;
第二类丢失更新:两个都提交了的事务,由于在不同的事务中,无法探知其他事务的操作,导致实际结果不符。

目前大部分数据库基本已经消灭了第一类丢失更新,对于第二类丢失更新,主要目标是克服事务之间协助的一致性,方法是在数据库标准规范中定义事务之间的隔离级别,来在不同程度上减少出现丢失更新的可能性。

隔离级别

隔离级别可以在不同程度上减少丢失更新。
按照SQL的标准规范,把隔离级别定义为4层,分别是:脏读(dirty read)读/写提交(read commit)可重复度(repeatable read)序列化(Serializable)

  • 脏读是最低的隔离级别,其含义是允许一个事务去读取另一个事务中未提交的数据
  • 读/写提交,就是说一个事务只能读取另一个事务已经提交的数据
  • 可重复读,是针对数据库同一条记录而言,换句话说,可重复读会使得同一条数据库记录的读/写按照一个序列化进行操作,不会产生交叉情况,这样就能保证同一条数据的一致性,进而解决不可重复读(unrepeatable read)的问题
  • 序列化,它是一种让SQL按照顺序读/写的方式,能够消除数据库事务之间并发产生数据不一致的问题,即幻读(phantom read)

选择隔离级别和传播行为

选择隔离级别的出发点在于两点:性能和数据一致性

选择隔离级别

一般而言,从脏读到序列化,系统性能直线下降。因此设置高的级别,比如序列化,会严重压制并发,从而引发大量的线程挂起,直到获得锁才能进一步操作,而恢复时又需要大量的等待时间。在大部分场景下,企业会选择读/写提交的方式设置事务,这样既有助于提高并发,又压制了脏读,但是对于数据一致性问题并没有解决。对于一般的应用都可以使用@Transactional方法进行配置,示例如下:

@Autowired
private RoleDao roleDao = null;
//设置方法为读/写提交的隔离级别
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED)
public int insertRole(Role role) {
   return roleDao.insert(role);
}

当业务并发量不是很大或者根本不需要考虑的情况下,使用序列化隔离级别用以保证数据的一致性,也是合理的。
总之,隔离级别需要根据并发的大小和性能来做出决定,对于并发不大又需要保证数据安全性的可以使用序列化的隔离级别,这样就能保证数据库在多事务环境中的一致性。代码如下:

@Autowired
private RoleDao roleDao = null;
// 设置方法为序列化的隔离级别
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.SERIALIZABLE)
public int insertRole(Role role) {
   return roleDao.insert(role);
}

注意,实际工作中,注解@Transactional隔离级别的默认值为Isolation.DEFAULT,其含义是默认的,随数据库默认值的变化而变化。不同的数据库隔离级别的支持是不一样的,MySQL支持4种隔离级别,默认是可重复读,而Oracle只支持读/写提交和序列化两种隔离级别,默认为读/写提交。

传播行为

传播行为是指方法之间的调用事务策略的问题。当一个方法调度另外一个方法时,可以对事务的特性进行传播配置,称之为传播行为。
在Spring中传播行为的类型,是通过一个枚举类型去定义的,这个枚举类是org.springframework.transaction.annotation.Propagation,它定义了7种传播行为,如下表所示:

传播行为 含义 备注
REQUIRED 当方法调用时,如果不存在当前事务,那么就创建事务;如果之前的方法已经存在事务了,那么就沿用之前的事务 这是Spring默认的传播行为
SUPPORTS 当方法调用时,如果不存在当前事务,那么不启用事务;如果存在当前事务,那么就沿用当前事务
MANDATORY 方法必须在事务内允许 如果不存在当前事务,那么就抛出异常
REQUIRES_NEW 无论是否存在当前事务,方法都会在新的事务中允许 也就是事务管理器会打开新的事务运行该方法
NOT_SUPPORTED 不支持事务,如果不存在当前事务也不会创建事务;如果存在当前事务,则挂起它,直到该方法结束后才恢复当前事务 适用于那些不需要事务的SQL
NEVER 不支持事务,只有在没有事务的环境中才能运行它 如果方法存在当前事务,则抛出异常
NESTED 嵌套事务,也就是调用方法如果抛出异常只回滚自己内部执行的SQL,而不回滚主方法的SQL 它的实现存在两种情况,如果当前数据库支持保存点(savepoint),那么它就会在当前事务上使用保存点技术;如果发生异常则将方法内执行的SQL回滚到保存点上,而不是全部回滚,否则就等同于REQUIRES_NEW创建新的事务运行方法代码

最常用的时REQUIRED,也是默认的传播行为。一般而言,企业比较关注的时REQUIRES_NEW和NESTED.

在Spring+MyBatis组合中使用事务

通过Spring和MyBatis的组合,给出一个较为详细的实例:
目录图如下:
在这里插入图片描述
文件作用表如下:
在这里插入图片描述
首先,搭建环境。环境配置的XML文件如下:

<?xml version='1.0' encoding='UTF-8' ?>
<!-- was: <?xml version="1.0" encoding="UTF-8"?> -->
<beans xmlns="http://www.springframework.org/schema/beans"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
   xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
   xmlns:context="http://www.springframework.org/schema/context"
   xsi:schemaLocation="http://www.springframework.org/schema/beans 
      http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
       http://www.springframework.org/schema/aop 
       http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
       http://www.springframework.org/schema/tx 
       http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
       http://www.springframework.org/schema/context 
       http://www.springframework.org/schema/context/spring-context-4.0.xsd">
   <!--启用扫描机制,并指定扫描对应的包-->
   <context:annotation-config />
   <context:component-scan base-package="com.ssm.chapter13.*" />
   <!-- 数据库连接池 -->
   <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
      <property name="driverClassName" value="com.mysql.jdbc.Driver" />
      <property name="url" value="jdbc:mysql://localhost:3306/chapter13"/>
      <property name="username" value="root" />
      <property name="password" value="123456" />
      <property name="maxActive" value="255" />
      <property name="maxIdle" value="5" />
      <property name="maxWait" value="10000" />
   </bean>

   <!-- 集成MyBatis -->
   <bean id="SqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
      <property name="dataSource" ref="dataSource" />
         <!--指定MyBatis配置文件-->
      <property name="configLocation" value="classpath:/mybatis/mybatis-config.xml" />
   </bean>

   <!-- 事务管理器配置数据源事务 -->
   <bean id="transactionManager"
      class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
      <property name="dataSource" ref="dataSource" />
   </bean>

   <!-- 使用注解定义事务 -->
   <tx:annotation-driven transaction-manager="transactionManager" />

   <!-- 采用自动扫描方式创建mapper bean -->
   <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
      <property name="basePackage" value="com.ssm.chapter13" />
      <property name="SqlSessionFactory" ref="SqlSessionFactory" />
      <property name="annotationClass" value="org.springframework.stereotype.Repository" />
   </bean>
   
</beans>

先给出数据库表映射的POJO类:

public class Role {
   private Long id;
   private String roleName;
   private String note;
   
   public Long getId() {
      return id;
   }
   public void setId(Long id) {
      this.id = id;
   }
   public String getRoleName() {
      return roleName;
   }
   public void setRoleName(String roleName) {
      this.roleName = roleName;
   }
   public String getNote() {
      return note;
   }
   public void setNote(String note) {
      this.note = note;
   }
}

搭建MyBatis的映射文件,建立SQL和POJO的关系:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ssm.chapter13.mapper.RoleMapper">
    <insert id="insertRole" parameterType="com.ssm.chapter13.pojo.Role">
        insert into t_role (role_name, note) values(#{roleName}, #{note})
    </insert>
</mapper>

配置一个接口:

import com.ssm.chapter13.pojo.Role;
import org.springframework.stereotype.Repository;

@Repository
public interface RoleMapper {
   public int insertRole(Role role);
}

为了引入这个映射器,需要配置一个MyBatis的配置文件:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <mappers>
        <mapper resource="com/ssm/chapter13/sqlMapper/RoleMapper.xml"/>
    </mappers>
</configuration>

到此MyBatis部分的内容已经配置完成,接着配置一些服务(Service)类。对于服务类,坚持“接口+实现类“的规则,先定义两个接口:

import java.util.List;
import com.ssm.chapter13.pojo.Role;

public interface RoleService {
   public int insertRole(Role role);
}
import java.util.List;
import com.ssm.chapter13.pojo.Role;

public interface RoleListService {
   public int insertRoleList(List<Role> roleList);
}

RoleService接口的insertRole方法可以对单个角色进行插入,而RoleListService的insertRoleList方法可以对角色列表进行插入。注意insertRoleList方法会调用insertRole,这样就可以测试各类的传播行为了。下面是两个接口的实现类:

import java.util.List;
import org.apache.log4j.Logger;
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;
import com.ssm.chapter13.pojo.Role;
import com.ssm.chapter13.service.RoleListService;
import com.ssm.chapter13.service.RoleService;
@Service
public class RoleListServiceImpl implements RoleListService {
   @Autowired
   private RoleService roleService = null;
   Logger log = Logger.getLogger(RoleListServiceImpl.class);
   @Override
   @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED)
   public int insertRoleList(List<Role> roleList) {
      int count = 0;
      for (Role role : roleList) {
         try {
            count += roleService.insertRole(role);
         } catch (Exception ex) {
            log.info(ex);
         }
      }
      return count;
   }
}
import java.util.List;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import com.ssm.chapter13.mapper.RoleMapper;
import com.ssm.chapter13.pojo.Role;
import com.ssm.chapter13.service.RoleService;

@Service
public class RoleServiceImpl implements RoleService, ApplicationContextAware {
   @Autowired
   private RoleMapper roleMapper = null;  
   @Override
   @Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_COMMITTED)
   public int insertRole(Role role) {
      return roleMapper.insertRole(role);
   }
}

两个服务实现类方法标注了@Transactinal注解,它们都会以对应的隔离级和传播行为中运行。
为了更好地测试从而输出对应的日志,修改log4j的配置文件:

log4j.rootLogger=DEBUG , stdout 
log4j.logger.org.springframework=DEBUG 
log4j.appender.stdout=org.apache.log4j.ConsoleAppender   
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout   
log4j.appender.stdout.layout.ConversionPattern=%5p %d %C: %m%n 

测试代码如下:

import java.util.ArrayList;
import java.util.List;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.ssm.chapter13.pojo.Role;
import com.ssm.chapter13.service.RoleListService;
public class Chapter13Main {
   public static void main(String [] args) {
      ApplicationContext ctx = new ClassPathXmlApplicationContext ("spring-cfg.xml");
      RoleListService roleListService = ctx.getBean(RoleListService. class);
      List<Role> roleList = new ArrayList<Role>();
        for (int i=1; i<=2; i++) {
            Role role = new Role();
            role.setRoleName("role_name_" + i);
            role.setNote("note_" + i);
            roleList.add(role);
        }
        int count = roleListService.insertRoleList(roleList);
      System.out.println(count);
   }
}

这里插入了两个角色,由于insertRoleList会调用insertRole,而insertRole标注了REQUIRES_NEW,所以每次调用会产生新的事务。

由于保存点技术并不是每一个数据库都能支持的,所以当你把传播行为设置为NESTED时,Spring会先去探测当前数据库是否能够支持保存点技术。如果数据库不予支持,它就会和REQUIRES_NEW一样创建新事务去运行代码,以达到内部方法发生异常时并不回滚当前事务的目的。

@Transactional的自调用失效问题

有时候配置了注解@Transactional,但是它会失效,这里要注意一些细节问题,以避免落入陷阱。

注解@Transaction的底层实现是Spring AOP技术,而Spring AOP技术使用的是动态代理。这就意味着对于静态(static)方法和非public方法,注解@Transactional是失效的。还有一个更为隐秘的,而且在使用过程中极其容易犯错误的——自调用
所谓自调用,就是一个类的一个方法去调用自身另外一个方法的过程。示例代码如下:

import java.util.List;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import com.ssm.chapter13.mapper.RoleMapper;
import com.ssm.chapter13.pojo.Role;
import com.ssm.chapter13.service.RoleService;

@Service
public class RoleServiceImpl implements RoleService, ApplicationContextAware {
   
   @Autowired
   private RoleMapper roleMapper = null;
   @Override
@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_COMMITTED)
public int insertRole(Role role) {
   return roleMapper.insertRole(role);
}

   //自调用问题
@Override
@Transactional(propagation = Propagation.REQUIRED, isolation=Isolation.READ_COMMITTED)
public int insertRoleList(List<Role> roleList) {
   int count = 0;
   for (Role role : roleList) {
      try {
               //调用自身类的方法,产生自调用问题
         insertRole(role);
         count++;
      } catch (Exception ex) {
         ex.printStackTrace();
      }
   }
   return count;
}
}

在insertRoleList方法的实现中,它调用了自身类实现insertRole的方法,而insertRole声明是REQUIRES_NEW的传播行为,也就是每次调用就会产生新的事务运行。

出现这个的问题根本原因在于AOP的实现原理。由于@Transactional的实现原理是AOP,而AOP的实现原理是动态代理,而自己调用自己的过程,并不存在代理对象的调用,这样就不会产生AOP去为我们设置@Transactional配置的参数,这样就出现了自调用注解失效的问题
为了克服这个问题,一方面可以使用两个服务类Spring IoC容器中为你生成了RoleService的代理对象,这样就可以使用AOP,且不会出现自调用的问题。另外一方面,你也可以直接从容器中获取Service的代理对象,从IoC容器中获取RoleService代理对象。代码如下:

//消除自调用问题
@Override
@Transactional(propagation = Propagation.REQUIRED, isolation= Isolation.READ_COMMITTED)
public int insertRoleList2(List<Role> roleList) {
   int count = 0;
   //从容器中获取RoleService对象,实际是一个代理对象
   RoleService service = ctx.getBean(RoleService.class);
   for (Role role : roleList) {
      try {
         service.insertRole(role);
         count++;
      } catch (Exception ex) {
         ex.printStackTrace();
      }
   }
   return count;
}

从容器获取代理对象的方法克服了自调用的过程,但是有一个弊端,就是从容器获取代理对象的方法有侵入之嫌,你的类需要依赖于Spring IoC容器,而这个类可以使用另一个服务类去调用。

典型错误用法的剖析

数据事务是企业应用关注的核心内容,也是开发者最容易犯错的问题,因此这里列举一些使用不良习惯,注意它们可以避免一些错误和性能的丢失。

错误使用Service

互联网往往采用模型—视图—控制器(Model View Controller,MVC)来搭建开发环境,因此在Controller中使用Service是十分常见的。

在Controller每调用一次带事务的service都会创建数据库事务。如果多次调用,则不在同一个事务中,这会造成不同时提交和回滚不一致的问题。每一个Java EE开发者都要注意这类问题,以避免一些不必要的错误。

过长时间占用事务

在企业的生产系统中,数据库事务资源是最宝贵的资源之一,使用了数据库事务之后,要及时释放数据库事务。换言之,我们应该尽可能地使用数据库事务资源去完成所需工作,但是在一些工作中需要使用到文件、对外连接等操作,而这些操作往往会占用较长时间,针对这些,如果开发者不注意细节,就很容易出现系统宕机的问题。
一些系统之间的通信及一些可能需要花费较长时间的操作,都要注意这个问题,放在controller层等事务外进行处理,以避免长时间占用数据库事务,导致系统性能的低下。

错误捕捉异常

带事务的service中,出现异常想要回滚时应抛出异常,而不是捕获。

发布了279 篇原创文章 · 获赞 169 · 访问量 32万+

猜你喜欢

转载自blog.csdn.net/ARPOSPF/article/details/105321290