什么是Spring的事务管理
在实际开发中,操作数据库时都会涉及到事务管理问题,为此Spring提供了专门用于事务处理的API。Spring的事务管理简化了传统的事务管理流程,并在一定程度上减少了开发者的工作量。
为Spring提供事务管理的jar包spring-tx
。
注意:
要使用Spring的事务管理,请先去了解数据库事务管理基础知识(并发、锁、隔离级别等等)。
ThreadLocal
从前面的文章我们知道,Spring通过各类模板类降低了开发者使用各种数据持久化技术的难度。这些模板类都是线程安全的,也就是说,多个DAO可以复用同一个模板实例而不会发生冲突。使用模板类访问底层数据,根据持久化技术的不同,模板类需要绑定数据连接或会话的资源。但这些资源本身是非线程安全的,也就是说它们不能在同一时刻被多个线程共享。虽然模板类通过资源获取数据连接或会话,但资源池本身解决的是数据连接或会话的缓存问题,并非数据连接或会话的线程安全问题。
按照传统经验,如果某个对象是非线程安全的,在多线程环境下,对对象的访问必须采用synchronized
进行线程同步。但模板类并未采用线程同步机制,因为线程同步会降低并发性,影响系统性能。此外,通过代码同步解决线程安全的挑战性很大,可能会增加几倍的实现难度。那么,模板类是用了什么魔法使得可以在无须线程同步的情况下就解决线程安全的难题呢?答案就是ThreadLocal
。
ThreadLocal
在Spring中发挥着重要的作用,在管理request作用域的Bean、事务管理、任务调度、AOP等模块都出现了它的身影。要想了解Spring事务管理的底层技术,必须明白ThreadLocal
是什么。
ThreadLocal是什么?
ThreadLocal
,顾名思义,它不是一个线程,而是保存线程本地化对象的容器,保存线程本地化对象的容器,保存线程本地化对象的容器!!!当运行于多线程环境的某个对象使用ThreadLocal
维护变量时,ThreadLocal
为每个使用该变量的线程分配一个独立的变量副本。所以每个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的变量副本。从线程的角度看,这个变量就像线程专用的本地变量。
可以总结为一句话:ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。
InheritableThreadLocal
继承与ThreadLocal
,它会自动为子线程复制一份从父线程那里继承而来的本地变量:在创建子线程时,子线程会接受所有可继承的线程本地变量的初始值。当必须将本地线程变量自动传送给所有创建的子线程时,应尽可能地使用;InheritableThreadLocal
,而非ThreadLocal
。
推荐一篇文章知乎:ThreadLocal和synchronized的区别?
ThreadLocal与synchronized比较
它们都是为了解决多线程中相同变量的访问冲突问题。那么,ThreadLocal
有什么优势?
在synchronized
同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序缜密地分析什么时候对变量进行读/写,什么时候需要锁定某个对象、什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。
而ThreadLocal
从另一个角度来解决多线程的并发访问。ThreadLocal为每个线程提供了一个独立的变量副本,从而隔离了多个线程对访问数据的冲突问题。因为每个线程都拥有自己的变量副本,因而也就没有必要对该变量进行同步。ThreadLocal
提供了线程安全的对象封装,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal
。
Spring中的ThreadLocal
我们知道在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中, 绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、 TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全状态采用 ThreadLocal进行处理,让它们也成为线程安全的状态,因为有状态的Bean就可以在多线程中共享了。
ThreadLocal总结
对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式:访问串行化、对象共享化;而ThreadLocal
采用了“以空间换时间”的方式:访问并行化,对象独享化。前者仅提供一份变量,让不同的线程排队访问;而后者为每个线程都提供了一份变量,因此可以同时访问而互不影响。
Spring的事务管理
Spring为事务管理提供了一致的编程模板,在高层次建立了统一的事务抽象。也就是说,不管是选择Spring JDBC、Hibernate、JPA还是选择Mybatis,Spring都可以让用户用统一的编程模型进行事务管理。
像Spring DAO为不同的持久化实现提供了模板类一样,Spring事务管理继承了这一风格,也提供了事务模板了TransactionTemplate
,通过它并配合使用事务回调TransactionCallback
指定具体的持久化操作,就可以通过编程方式实现事务管理,而无需关注资源获取、复用、释放、事务同步和异常处理等操作。
Spring事务管理的亮点在于声明式事务管理。Spring允许通过声明方式,在IOC配置中指定事务的边界和事务属性,Spring自动在指定的事务边界上应用事务属性。
事务管理的核心接口
在Spring事务管理SPI(Service Provider Interface)的抽象层主要包括3个接口,分别是PlatformTransactionManager
、TransactionDefinition
和TransactionStatus
,它们位于org.springframework.transaction
包中,三者间的关系如下:
TransactionDefinition
用于描述事务的隔离级别、超时时间、是否为只读事务和事务传播规则等控制事务具体行为的事务属性,这些事务实现可以通过XML配置或注解描述提供,也可以通过手工编程的方式设置。PlatformTransactionManager
根据TransactionDefinition
提供的事务属性配置信息创建事务,并用TransactionStatus
描述这个激活事务的状态。下面分别描述这些接口:
TransactionDefinition
该接口定义了Spring兼容的事务属性,这些属性对事务管理控制的若干方面进行配置。
- 事务隔离:当前事务和其他事务的隔离程度。
- 事务传播:通常在一个事务中执行的所有代码都会允许在同一事务上下文中。
- 事务超时:事务在超时前能允许多久,超过时间后,事务被回滚。
-
只读状态:只读事务不修改任何数据,资源事务管理者可以针对可读事务应用一些优化措施,提高运行性能。
Spirng允许通过XML或注解元数据的方式为一个有事务要求的服务类方法配置事务属性,这些信息作为Spring事务管理框架的“输入”,Spring将自动按事务属性信息的指示,为目标方法提供相应的事务支持。
TransactionStatus
该接口代表一个事务的具体运行状态。事务管理者可以通过该接口获取事务运行期的状态信息,也可以通过该接口间接地回滚事务,它相比于抛出异常时回滚事务的方式更具可控性。
PlatformTransactionManager
通过JDBC的事务管理知识可以知道,事务只能被提交或回滚(或回滚到某个保存点后提交),而该接口很好地描述了事务管理这个概念,解释见代码注释:
public interface PlatformTransactionManager {
//该方法根据事务定义信息从事务环境中返回一个已存在的事务,或者创建一个新的事务,并用TransacitonStatus描述这个事务的状态
TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;
//根据事务的状态提交事务。如果事务已经被标识为rollback-only,则该方法将执行一个回滚事务的操作
void commit(TransactionStatus var1) throws TransactionException;
//将事务回滚。当commit()方法抛出异常时,该方法会被隐式调用
void rollback(TransactionStatus var1) throws TransactionException;
}
事务管理器实现类
Spring将事务管理委托给底层具体的持久化实现框架来完成。因此为不同的框架提供了PlatformTransactionManager
接口的实现类,如下图
这些事务管理器都是对特定事务实现框架的代理,这样就可以通过Spring所提交的高级抽象对不同种类的事务实现使用相同的方式进行管理,而不同关心具体的实现。
要实现事务管理,首先要在Spring中配置好相应的事务管理器,为事务管理器指定数据资源及一些其他事务管理控制属性,下面列出常见框架的配置。
1.Spring JDBC 和Mybatis框架配置
如果使用Spring JDBC或Mybatis,由于它们都基于数据源的Connection访问数据库,所以可以使用DataSourceTransactionManager
,只要在Spring中进行如下配置即可:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
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">
<!--1.加载指定文件以配置数据库相关参数属性:${xxx}-->
<context:property-placeholder location="classpath:jdbc.properties"/>
<!--2.定义数据源-->
<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close"
p:driverClassName="${jdbc.driverClass}"
p:url="${jdbc.url}"
p:username="${jdbc.username}"
p:password="${jdbc.password}"
/>
<!--3.基于相应数据源的事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
p:dataSource-ref="dataSource" />
</beans>
2.Hibernate框架配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
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">
<!--1.加载指定文件以配置数据库相关参数属性:${xxx}-->
<context:property-placeholder location="classpath:jdbc.properties"/>
<!--2.定义数据源,配置连接池属性和c3p0私有属性-->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close"
p:driverClass="${jdbc.driverClass}"
p:jdbcUrl="${jdbc.url}"
p:user="${jdbc.username}"
p:password="${jdbc.password}"
p:maxPoolSize="30"
p:minPoolSize="10"
p:autoCommitOnClose="false"
p:checkoutTimeout="10000"
p:acquireRetryAttempts="2"
/>
<!--3.配置SqlSessionFactoryBean对象:注入数据源,配置mybatis全局文件,扫描entity包,使用别名,扫描sql配置文件-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"
p:dataSource-ref="dataSource"
p:configLocation="classpath:mybatis-config.xml"
p:typeAliasesPackage="cn.wk.entity"
p:mapperLocations="classpath:mapper/*.xml"
/>
<!--4.配置扫描Dao接口包,动态实现Dao接口,注入到Spring容器中-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"
p:sqlSessionFactoryBeanName="sqlSessionFactory"
p:basePackage="cn.wk.dao"
/>
</beans>
事务同步管理器(暂时了解)
Spring将JDBC的Connection、Hibernate的Session等访问数据库的连接或会话统称为资源,这些资源在同一时刻是不能多线程共享的。为了让DAO、Service类可以做到singleton,Spring的事务同步管理器类org.springframework.transaction.support.TransactionSynchronizationManager
使用ThreadLocal
为不同事务线程提供了独立的资源副本,同时维护事务配置的属性和运行状态信息。
Spring框架为不同的持久化技术提供了一套从TransactionSynchronizationManager
获取对应线程绑定资源的工具类,如下图:
这些工具类都提供了静态的方法,通过这些方法可以获取和当前线程绑定的资源。
事务传播行为
当我们调用一个基于Spring 的Service接口方法(如UserService
的adduser()
方法)时,它将运行于Spring管理的事务环境中,Service接口方法可能会在内部调用其他的Service接口方法以共同完成一个完整的业务操作,因此就会产生服务接口方法嵌套调用的情况,Spring通过事务传播行为控制当前的事务如何传播到被签到调用的目标服务接口方法中。
Spring在TransactionDefinition
接口中规定了7中类型的事务传播行为,它们规定了事务方法和事务方法发生嵌套调用时事务如何进行传播,如下图:
Spring事务管理的两种方式
- 编程式事务管理:通过编写代码实现的事务管理,包括定义事务的开始、正常执行后的事务提交和异常时的事务回滚。
- 声明式事务管理:通过AOP技术实现的事务管理,主要思想是将事务作为一个“切面”代码单独编写,然后通过AOP技术将事务管理的“切面”植入到业务目标类中。
声明式事务管理的最大优点在于开发者无需通过编程的方式来管理事务,只需在配置文件中进行相关的事务规则声明,就可以将事务应用到业务逻辑中。这使得开发人员可以更加专注于核心业务逻辑代码的编写,在一定程度上减少了工作量,提高了开发效率,所以在实际开发中,通常都推荐使用声明式事务管理。
编程式的事务管理(略)
声明式事务管理
大多数Spring用户选择声明式事务管理的功能,这种方式对代码的侵入性最小,可以让事务管理代码完全从业务代码中移除,非常复合非侵入式轻量级容器的理念。
Spring的声明式事务管理是通过SpringAOP实现的,通过事务的声明式信息,Spring负责将事务管理增强逻辑东塔织入业务方法的相应连接点中。这些逻辑包括获取线程绑定资源、开始事务、提交/回滚事务、进行异常转换和处理等工作。
在Spring早期版本中,用户必须通过TransactionProxyFactoryBean
代理类对需要事务管理的业务类进行代理,以便实施事务功能的增强。现在我们可以通过aop/tx
命名空间声明事务,因此代理类实施声明式事务的方法基本不再使用。当然,了解TransactionProxyFactoryBean
有助于我们更直观地理解Spring实施声明式事务的内部工作原理,通过一个例子来了解:
1.创建一个业务Bean
@Service
@Transactional
public class BbtForum{
public ForumDao forumDao;
public TopicDao topicDao;
public PostDao postDao;
public void addTopic(Top topic){
topicDao.addTopic(topic);
postDao.addPost(topic.getPost());
}
public forum getForun(int forumId){
return forumDao.getForum(forumId);
}
public void updateForum(Forum forum){
forumDao.updateForum(forum);
}
public int getForumNum(){
return forumDao.getForumNum();
}
}
该类有4个方法,我们希望addTopic
和updateForum()
方法拥有写事务的能力,而其他两个方法只需要有读事务的能力就可以了。
2.使用TransactionProxyFactoryBean配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
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">
<!--1.加载指定文件以配置数据库相关参数属性:${xxx}-->
<context:property-placeholder location="classpath:db.properties"/>
<!--2.定义数据源-->
<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close"
p:driverClassName="${jdbc.driverClass}"
p:url="${jdbc.url}"
p:username="${jdbc.username}"
p:password="${jdbc.password}"
/>
<!--3.基于相应数据源的事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
p:dataSource-ref="dataSource" />
<!--需要实施事务增强的业务Bean-->
<bean id="bbtForumTarget" class="cn.wk.chapter11.service.BbtForum"
p:forumDao-ref="forumDao"
p:topicDao-ref="topicDao"
p:postDao-ref="postDao"
/>
<!--使用事务代理工厂为目标业务Bean提供事务增强-->
<bean id="bbtForum" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"
p:transactionManager-ref="transactionManager"
p:target-ref="bbtForumTarget">
<property name="transactionAttributes">
<props>
<!--只读事务-->
<prop key="get*">PROPAGATION_REQUIRED,readOnly</prop>
<!--可写事务-->
<prop key="*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
</beans>
按照约定的习惯,需要事务增强的业务类一般将id取名为xxTarget,这可以在字面上表示该Bean是要被代理的目标Bean。
通过TransactionProxyFactoryBean
对业务类进行代理,织入事务增强功能。首先,需要为该代理类指定事务管理器,这些事务管理器实现了PlatformTransactionManager
接口;其次,通过target属性指定需要代理的目标Bean;最后,为业务Bean的不同方法配置事务属性。Spring允许通过键值配置业务方法的事务属性信息,键可以使用通配符,如get*
代表目标类中所有以get为前缀的方法,它匹配BbtForum
的getForum(int forumId)
和getForumNum()
方法;而key="*"
代表匹配BbtForum
接口的所有方法。
<prop>
内的值为事务属性信息,配置格式如下:
基于XML配置的声明式事务
使用TransactionProxyFactoryBean
代理工厂类为业务类添加事务支持缺点是配置过于繁琐,所以Spring后来在配置中添加了一个tx命名空间,在配置文件中以明确结构化的方式定义事务属性,大大提高了配置事务属性的便利性,<tx:advice>
标签用法格式如下:
我们用tx和aop命名空间对前面基于FactoryBean的事务配置方式进行替换,代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
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
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-context.xsd">
<!--1.加载指定文件以配置数据库相关参数属性:${xxx}-->
<context:property-placeholder location="classpath:db.properties"/>
<!--2.定义数据源-->
<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close"
p:driverClassName="${jdbc.driverClass}"
p:url="${jdbc.url}"
p:username="${jdbc.username}"
p:password="${jdbc.password}"
/>
<!--3.基于相应数据源的事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
p:dataSource-ref="dataSource"/>
<!--4.使用强大的切点表达式语言轻松定义目标方法-->
<aop:config>
<!--通过aop定义事务增强切面-->
<aop:pointcut id="serviceMethod" expression="execution(* cn.wk.chapter11.service.*Forum.*(..))"/>
<!--引用事务增强-->
<aop:advisor advice-ref="serviceMethod" advice-ref="txAdvice"/>
</aop:config>
<!--5.事务增强-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<!--属性定义-->
<tx:attributes>
<tx:method name="get" read-only="false"/>
<tx:method name="add*" rollback-for="Exception"/>
<tx:method name="update"/>
</tx:attributes>
</tx:advice>
</beans>
在这一过程中我们看到了3种角色:通过aop/tx定义的声明式事务配置信息、业务Bean、Spring容器。Spring容器自动将第一者应用于第二者,从容器中返回的业务Bean已经是被织入事务增强的代理Bean,即第一者和第二者在配置时不直接发生关系。
而在使用TransactionProxyFactoryBean
进行事务配置时,它需要直接通过target属性引用目标业务Bean,结果造成目标业务Bean往往需要使用target进行命名,以避免和最终代理Bean名冲突。使用aop/tx方式后,业务Bean的名称不需要做任何“配合性”的调整,aop直接通过切点表达式语言就可以对业务Bean进行定位。从这个意义声来说,aop/tx
的配置方式对业务Bean是“无侵入”的,而代理类的配置显然是“侵入式”的。
在aop的命名空间中,通过切点表达式语言,将cn.wk.chapter11.service包下所有以Forum为后缀的类纳入了需要进行事务增强的范围,并配合<tx:advice>
和<aop:advisor>
完成了事务切面的定义。<aop:advisor>
引用的txAdvice增强是在tx命名空间上定义的。首先,事务增强一定需要一个事务管理器的支持,<tx;advice>
通过transaction
属性引用了定义好的事务管理器。曾经掺杂在一起,以逗号分割字符串定义的事务属性,现在变成了一个清晰的XML片段,十分简洁。<tx:method>
元素用于的属性如下:
如果需要为不同的业务类Bean应用不同的事务管理风格,则可以在
<aop;config>
中定义另外多套事务切面,具体的配置方法在现有基础上演绎即可。
基于注解的声明式事务(常用)
除了基于XML的事务配置,Spring还提供了基于注解的事务配置,即通过@Transactional
对需要事务增强的Bean接口、实现类或方法进行标注;在容器中配置基于注解的事务增强驱动,即可启用基于注解的声明式事务。现在项目中基本都采用这种配置。下面为配置步骤:
1.在需要事务管理的业务Bean前加上一个@Transactional
注解:
@Service
@Transactional
public class BbtForum{
.......
}
2.在配置文件加入标签
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
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
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-context.xsd">
<!--1.扫描service包注册以注解方式声明的Bean-->
<context:component-scan base-package="cn.wk.crm.service"/>
<!--2.配置事务管理器,注入数据库连接池-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
p:dataSource-ref="dataSource"
/>
<!--3.注解驱动会对标注@Transactional的Bean进行加工处理,以织入事务管理切面-->
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
注:
数据源一般在dao
层的xml文件配置,此处直接引用了,详见Spring第三篇文章。
在默认情况下,<tx:annotation-driven>
会默认使用名为transactionManager
的事务管理器。所以,如果用户的事务管理器id为transactionManager
,则可以进一步简化配置为:
<tx:annotation-driven/>
@Transactional注解属性
和XML配置方式一样,该注解也拥有一组普适性很强的默认事务属性,往往可以直接使用这些默认属性,具体如下:
-
事务传播行为:
PROPAGATION_REQUIRED
-
事务隔离级别:
ISOLATION_DEFAULT
- 读写事务属性:读/写事务。
- 超时时间:依赖于底层的事务系统的默认值。
- 回滚设置:任何运行期异常引发回滚,任何检查型异常不会引发回滚。
这些默认设置在大多数情况下都适用,当然,Spring也运行通过手工设定属性值覆盖默认值。
@Transactional注解的两个位置
@Transactional注解可以在类上使用,表示事务的设置对整个类上的方法都起作用,也可以在方法处使用,表示事务的设置只对该方法有效,如果即在类上加上该注解,又在类中方法上加上该注解,则方法上的注解会覆盖类上的注解。
参考资料
《精通Spring 4.x 企业应用开发》