概述
Spring为事务管理提供了一致的编程模板,在高层次建立了统一的事务抽象。也就是说,不管选择Spring JDBC、Hibernate 、JPA 还是iBatis,Spring都让我们可以用统一的编程模型进行事务管理。
类似Spring DAO 为不同的持久化技术实现提供了模板类一样,Spring事务管理也提供了事务模板类TransactionTemplate。 通过TransactionTemplate并配合使用事务回调TransactionCallback指定具体的持久化操作,就可以 通过编程的方式实现事务管理,而无须关注资源获取、复用、释放、事务同步和异步处理等操作。
Spring事务管理的亮点在于声明式事务管理,Spring允许通过声明的方式,在IoC配置中指定事务的边界和事务属性,Spring会自动在指定的事务边界上应用事务属性。
事务管理关键抽象
在Spring事务管理SPI(Service Provider Interface)的抽象层主要包括3个接口,分别是PlatformTransactionManager、TransactionDefinition和TransactionStatus。 都在org.springframework.transaction包中。
TransactionDefinition用于描述事务的隔离级别、超时时间、是否为只读事务和事务传播规则等控制事务具体行为的事务属性,这些事务属性可以通过XML配置或注解描述提供,也可以通过手工编程的方式设置。
PlatformTransactionManager根据TransactionDefinition提供的事务属性配置信息,创建事务,并用TransactionStatus描述这个激活事务的状态。
Spring事务管理的实现类
spring将事务管理委托底层具体的持久化实现框架去完成,因此针对不同的框架spring有的不同的接口实现类.
事务 | 说明 |
---|---|
org.springframework.orm.jpa.JpaTransactionManager | 使用JPA进行持久化时,使用该事务管理器 |
org.springframework.orm.hibernateX.HibernateTransactionManager | 使用HibernateX版本时使用该事务管理器 |
org.springframework.jdbc.datasource.DataSourceTransactionManager | 使用SpringJDBC或MyBatis等基于DataSource数据源的持久化技术时,使用该事务管理器 |
org.springframework.orm.jdo.JdoTransactionManager | 使用JDO进行持久化时,使用该事务管理器 |
org.springframework.transaction.jta.JtaTransactionManager | 具有多个数据源的全局事务使用该事务管理器(不管采用何种持久化技术) |
要实现事务管理,首先要在Spring中配置好相应的事务管理器,为事务管理器指定数据资源及一些其他事务管理控制属性。
下面介绍一下几个常见的事务管理器的配置
Spring JDBC 和MybBatis的事务管理器的配置
Spring JDBC 和MybBatis都是基于数据源的Connection访问数据库,所有都可以使用DataSourceTransactionManager, 配置如下
<!--引用外部的Properties文件-->
<context:property-placeholder location="classpath:jdbc.properties"/>
<!--配置一个数据源-->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close"
p:driverClassName="${jdbc.driverClassName}"
p:url="${jdbc.url}"
p:username="${jdbc.username}"
p:password="${jdbc.password}"/>
<!--基于数据源的事务管理器,通过属性引用数据源-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
p:dataSource-ref="dataSource"/>
JPA的事务管理器的配置
要配置一个JPA事务管理器,必须现提供一个DataSource,然后配置一个EntityManagerFactory,最后才配置JpaTransationManager.
.......
<!--通过dataSource-ref指定一个数据源-->
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"
p:dataSource-ref="dataSource"/>
......
</bean>
<!--指定实体管理器-->
<bean id="transactionManger" class="org.springframework.orm.jpa.JpaTransactionManager"
p:entityManagerFacotry-ref="entityManagerFactory"/>
Hibernate的事务管理器的配置
Spring4.0已经取消了对Hibernate3.6之前的版本支持,并全面支持Hibernate5.0. 因此,只为Hibernate3.6+提供事务管理器。
以Hibernate4.0为例
....
<!--通过dataSource-ref引用数据源 和 Hibernate配置文件 及其他属性-->
<bean id="sessionFactory"
class="org.springframework.orm.hibernate3.LocalSessionFactoryBean"
p:dataSource-ref="dataSource"
p:mappingResources="classpath:Artisan.hbm.xml">
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop>
<prop key="hibernate.show_sql">true</prop>
<prop key="hibernate.generate_statistics">true</prop>
</props>
</property>
</bean>
<bean id="transactionManager" class="org.springframework.orm.hibernate4.HibernateTransactionManager"
p:sessionFactory-ref="sessionFactory"/>
JTA 的事务管理器的配置
如果希望在JavaEE容器中使用JTA,则将通过JNDI和Spring的JtaTransactionManager获取一个容器的DataSource。
<!--通过jee命名空间获取Java EE应用服务器容器中的数据源-->
<jee:jndi-lookup id="accountDs" jndi-name="java:comp/env/jdbc/account"/>
<jee:jndi-lookup id="orderDs" jndi-name="java:comp/env/jdbc/account"/>
<!--指定JTA事务管理器。-->
<bean id="transactionManager"
class="org.springframework.transaction.jta.JtaTransactionManager"/>
事务同步管理器
Spring将JDBC的Connection、Hibernate的Session等访问数据库的连接或者会话对象统称为资源,这些资源在同一时刻是不能多线程共享的。
为了让DAO、Service类可能做到singleton, Spring的事务同步管理类org.springframework.transaction.support.TransactionSynchronizationManager使用ThreadLocal为不同事务线程提供了独立的资源副本,同时维护事务配置的属性和运行状态信息。
事务同步管理器是Spring事务管理的基石,不管用户使用的是编程式事务管理,还是声明式事务管理,都离不开事务同步管理器。
Spring框架为不同的持久化技术提供了一套从TransactionSynchronizationManager中获取对应线程绑定资源的工具类
持久化技术 | 线程绑定资源获取工具 |
---|---|
Spring JDBC或者MyBatis | org.springframework.jdbc.datasource.DataSourceUtils |
HibernateX.0 | org.springframework.orm.hibernateC.SessionFactoryUtils |
JPA | org.springframework.orm.jpa.EntityManagerFactoryUtils |
JDO | org.springframework.orm.jdo.PersistenceManagerFactoryUtils |
这些工具类都提供了静态的方法,通过这些方法可以获取和当前线程绑定的资源,如
DataSourceUtils.getConnection (DataSource
dataSource)可以从指定的数据源中获取和当前线程绑定的ConnectionHibernate的SessionFactoryUtils.getSession (SessionFactory
sessionFactory, boolean allowCreate)则从指定的SessionFactory中获取和当前线程绑定的Session。当需要脱离模板类,手工操作底层持久技术的原生API时,就需要通过这些工具类获取线程绑定的资源,而不应该直接从DataSource或SessionFactory中获取。因为后者不能获得和本线程相关的资源,因此无法让数据操作参与到本线程相关的事务环境中。
这些工具类还有另外一个重要的用途:将特定异常转换为Spring的DAO异常。
Spring为不同的持久化技术提供了模板类,模板类在内部通过资源获取工具类间接访问TransactionSynchronizationManager中的线程绑定资源。所以,如果Dao使用模板类进行持久化操作,这些Dao就可以配置成singleton。如果不使用模板类,也可直接通过资源获取工具类访问线程相关的资源。
我们来开下TransactionSynchronizationManager的面纱:
TransactionSynchronizationManager将Dao、Service类中影响线程安全的所有“状态”统一抽取到该类中,并用ThreadLocal进行替换,从此Dao(必须基于模板类或资源获取工具类创建的Dao)和Service(必须采用Spring事务管理机制)摘掉了非线程安全的帽子,完成了脱胎换骨式的身份转变。
事务的传播行为
当我们调用一个基于Spring的Service接口方法(如UserService#addUser())时,它将运行于Spring管理的事务 环境中,Service接口方法可能会在内部调用其它的Service接口方法以共同完成一个完整的业务操作,因此就会产生服务接口方法嵌套调用的情况, Spring通过事务传播行为控制当前的事务如何传播到被嵌套调用的目标服务接口方法中。
事务传播是Spring进行事务管理的重要概念,其重要性怎么强调都不为过。但是事务传播行为也是被误解最多的地方,在本文里,我们将详细分析不同事务传播行为的表现形式,掌握它们之间的区别。
Spring在TransactionDefinition接口中规定了7种类型的事务传播行为,它们规定了事务方法和事务方法发生嵌套调用时事务如何进行传播:
事务传播行为类型 | 说明 |
---|---|
PROPAGATION_REQUIRED | 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择 |
PROPAGATION_SUPPORTS | 支持当前事务,如果当前没有事务,就以非事务方式执行。 |
PROPAGATION_MANDATORY | 使用当前的事务,如果当前没有事务,就抛出异常。 |
PROPAGATION_REQUIRES_NEW | 新建事务,如果当前存在事务,把当前事务挂起 |
PROPAGATION_NOT_SUPPORTED | 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起 |
PROPAGATION_NEVER | 以非事务方式执行,如果当前存在事务,则抛出异常。 |
PROPAGATION_NESTED | 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。 |
当使用PROPAGATION_NESTED时,底层的数据源必须基于JDBC 3.0,并且实现者需要支持保存点事务机制。
示例
当服务接口方法分别使用表1中不同的事务传播行为,且这些接口方法又发生相互调用的情况下,大部分组合都是一目了然,容易理解的。但是,也存在一些容易引起误解的组合事务传播方式。
下面,我们通过两个具体的服务接口的组合调用行为来破解这一难点。这两个服务接口分别是UserService和ForumService, UserSerice有一个addCredits()方法,ForumSerivce#addTopic()方法调用了 UserSerice#addCredits()方法,发生关联性服务方法的调用:
@Service
public class ForumService {
private UserService userService;
// ①调用其它服务接口的方法
public void addTopic() {
// ②被关联调用的业务方法
userService.addCredits();
}
public void setUserService(UserService userService) {
this.userService = userService;
}
}
嵌套调用的事务方法 : 对Spring事务传播行为最常见的一个误解是:当服务接口方法发生嵌套调用时,被调用的服务方法只能声明为 PROPAGATION_NESTED。这种观点犯了望文生义的错误,误认为PROPAGATION_NESTED是专为方法嵌套准备的。这种误解遗害不 浅,执有这种误解的开发者错误地认为:应尽量不让Service类的业务方法发生相互的调用,Service类只能调用DAO层的DAO类,以避免产生嵌 套事务。
其实,这种顾虑是完全没有必要的,PROPAGATION_REQUIRED已经清楚地告诉我们:事务的方法会足够“聪明”地判断上下文是否已经存在一个事务中,如果已经存在,就加入到这个事务中,否则创建一个新的事务。
依照上面的例子,假设我们将ForumService#addTopic()和UserSerice#addCredits()方法的事务传播行为都设置为PROPAGATION_REQUIRED,这两个方法将运行于同一个事务中。
将ForumService#addTopic()设置为PROPAGATION_REQUIRED时, UserSerice#addCredits()设置为PROPAGATION_REQUIRED、PROPAGATION_SUPPORTS、 PROPAGATION_MANDATORY时,运行的效果都是一致的(当然,如果单独调用addCredits()就另当别论了)。
当addTopic()运行在一个事务下(如设置为PROPAGATION_REQUIRED),而addCredits()设置为 PROPAGATION_NESTED时,如果底层数据源支持保存点,Spring将为内部的addCredits()方法产生的一个内嵌的事务。如果 addCredits()对应的内嵌事务执行失败,事务将回滚到addCredits()方法执行前的点,并不会将整个事务回滚。内嵌事务是内层事务的一 部分,所以只有外层事务提交时,嵌套事务才能一并提交。
嵌套事务不能够提交,它必须通过外层事务来完成提交的动作,外层事务的回滚也会造成内部事务的回滚。
嵌套事务和新事务
PROPAGATION_REQUIRES_NEW 和 PROPAGATION_NESTED也是容易混淆的两个传播行为。PROPAGATION_REQUIRES_NEW 启动一个新的、和外层事务无关的“内部”事务。该事务拥有自己的独立隔离级别和锁,不依赖于外部事务,独立地提交和回滚。当内部事务开始执行时,外部事务 将被挂起,内务事务结束时,外部事务才继续执行。
由此可见, PROPAGATION_REQUIRES_NEW 和 PROPAGATION_NESTED 的最大区别在于:
- PROPAGATION_REQUIRES_NEW 将创建一个全新的事务,它和外层事务没有任何关系,
- 而 PROPAGATION_NESTED 将创建一个依赖于外层事务的子事务,当外层事务提交或回滚时,子事务也会连带提交和回滚。
以下几个问题值得注意:
1.当业务方法被设置为PROPAGATION_MANDATORY时,它就不能被非事务的业务方法调用。
如将ForumService#addTopic ()设置为PROPAGATION_MANDATORY,如果展现层的Action直接调用addTopic()方法,将引发一个异常。正确的情况是: addTopic()方法必须被另一个带事务的业务方法调用(如ForumService#otherMethod())。所以 PROPAGATION_MANDATORY的方法一般都是被其它业务方法间接调用的。
2 当业务方法被设置为PROPAGATION_NEVER时,它将不能被拥有事务的其它业务方法调用。
假设UserService#addCredits ()设置为PROPAGATION_NEVER,当ForumService# addTopic()拥有一个事务时,addCredits()方法将抛出异常。所以PROPAGATION_NEVER方法一般是被直接调用的。
3 当方法被设置为PROPAGATION_NOT_SUPPORTED时,外层业务方法的事务会被挂起,当内部方法运行完成后,外层方法的事务重新运行。如果外层方法没有事务,直接运行,不需要做任何其它的事。
在Spring声明式事务管理的配置中,事务传播行为是最容易被误解的配置项,原因在于事务传播行为名称(如 PROPAGATION_NESTED:嵌套式事务)和代码结构的类似性上(业务类方法嵌套调用另一个业务类方法).
编程式的事务管理
在实际的应用中很少通过编程来进行事务管理,但是Spring还是为编程式事务管理提供了模板类 TransactionTemplate,以满足一些特殊场合的要求。
TransactionTemplate是线程安全的,因此可以在多个类中共享TransactionTemplate实例进行事务管理。
TransactionTemplate主要有两个方法:
public void setTransactionManager(PlatformTransactionManager transactionManager) 设置事务管理器
public <T> T execute(TransactionCallback<T> action) throws TransactionException
在TransactionCallback回调接口中定义需要以事务方式组织的数据访问逻辑
TransactionCallback接口中仅有一个方法
protected void doInTransaction(TransactionStatus status)
如果操作不需要返回结果,可以使用TransactionCallback的子接口 TransactionCallbackWithoutResult。
示例
代码已托管到Github—> https://github.com/yangshangwei/SpringMaster
POJO
package com.xgj.dao.transaction.programTrans;
import org.springframework.stereotype.Component;
/**
*
*
* @ClassName: Artisan
*
* @Description: @Component标注的Bean
*
* @author: Mr.Yang
*
* @date: 2017年9月18日 下午5:03:47
*/
@Component
public class Artisan {
private String userName;
private String password;
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
package com.xgj.dao.transaction.programTrans;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
/**
*
*
* @ClassName: ProgramTransService
*
* @Description: 在实际应用中,很少通过编程的方式来进行事务管理。
*
* @author: Mr.Yang
*
* @date: 2017年9月21日 下午3:48:10
*/
@Service
public class ProgramTransService {
private JdbcTemplate jdbcTemplate;
private TransactionTemplate transactionTemplate;
// 下面两条SQL在一个事务中,第二条故意写错了表名,会执行失败,第一条已经成功的SQL也会回滚
private static final String addArtisanSQL = "insert into artisan_user(user_name,password) values(?,?)";
private static final String deleteOneArtisanSQL = "delete from artisan_user1 where user_name = 'ArtisanBatch0' ";
@Autowired
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
/**
*
*
* @Title: setTransactionTemplate
*
* @Description: 通过AOP主动注入transactionTemplate
*
* @param transactionTemplate
*
* @return: void
*/
@Autowired
public void setTransactionTemplate(TransactionTemplate transactionTemplate) {
this.transactionTemplate = transactionTemplate;
}
public void operArtisanInTrans(final Artisan artisan) {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
// 需要在事务中执行的逻辑
jdbcTemplate.update(addArtisanSQL, artisan.getUserName(),
artisan.getPassword());
System.out.println("addArtisanSQL OK ");
jdbcTemplate.update(deleteOneArtisanSQL);
System.out.println("deleteOneArtisanSQL OK ");
}
});
}
}
配置文件
<?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:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 扫描类包,将标注Spring注解的类自动转化Bean,同时完成Bean的注入 -->
<context:component-scan base-package="com.xgj.dao.transaction.programTrans" />
<!-- 不使用context命名空间,则需要定义Bean
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations" value="classpath:spring/jdbc.properties" />
</bean> -->
<!-- 使用context命名空间,同上面的Bean等效.在xml文件中配置数据库的properties文件 -->
<context:property-placeholder location="classpath:spring/jdbc.properties" />
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close"
p:driverClassName="${jdbc.driverClassName}"
p:url="${jdbc.url}"
p:username="${jdbc.username}"
p:password="${jdbc.password}" />
<!-- 配置Jdbc模板 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"
p:dataSource-ref="dataSource" />
<!--基于数据源的事务管理器,通过属性引用数据源-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
p:dataSource-ref="dataSource"/>
<!-- 配置transactionTemplate模板 -->
<bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate"
p:transactionManager-ref="transactionManager"/>
</beans>
单元测试
package com.xgj.dao.transaction.programTrans;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class ProgramTransServiceTest {
ClassPathXmlApplicationContext ctx = null;
@Before
public void initContext() {
// 启动Spring 容器
ctx = new ClassPathXmlApplicationContext(
"classpath:com/xgj/dao/transaction/programTrans/conf_program_transaction.xml");
System.out.println("initContext successfully");
}
@Test
public void testProgramTransaction() {
Artisan artisan = ctx.getBean("artisan", Artisan.class);
artisan.setUserName("trans");
artisan.setPassword("123");
ProgramTransService programTransService = ctx.getBean(
"programTransService", ProgramTransService.class);
programTransService.operArtisanInTrans(artisan);
System.out.println("testProgramTransaction successsfully");
}
@After
public void closeContext() {
if (ctx != null) {
ctx.close();
}
System.out.println("close context successfully");
}
}
运行结果
第二条因为执行失败,第一条也回滚了,未插入数据, OK。