(四)Spring 事务管理

前言

  在实际开发中,操作数据库时都会涉及到事务管理问题,为此 Spring 提供了专门用于事务处理的API。
  Spring 的事务管理通过 AOP 的思想简化了传统的事务管理流程,并在一定程度上减少了开发者的工作量。
  在 Spring 中,提供事务管理的jar包为spring-tx

注意哦:要使用Spring的事务管理,请先去了解数据库事务管理基础知识(并发、锁、隔离级别等))。

Spring 的事务管理

  Spring 为事务管理提供了一致的编程模板,在高层次建立了统一的事务抽象。也就是说,不管是选择 Spring JDBC、Hibernate、JPA 还是选择 Mybatis,Spring 都可以让用户用统一的编程模型进行事务管理。
  像 Spring DAO 为不同的持久化实现提供了模板类一样,Spring事务管理继承了这一风格,也提供了事务模板了TransactionTemplate,通过它并配合使用事务回调TransactionCallback指定具体的持久化操作,就可以通过编程方式实现事务管理,而无需关注资源获取、复用、释放、事务同步和异常处理等操作。
  Spring 事务管理的亮点在于声明式事务管理。Spring允许通过声明方式,在IOC配置中指定事务的边界和事务属性,Spring自动在指定的事务边界上应用事务属性。

事务管理的核心接口

  在 Spring 事务管理 SPI(Service Provider Interface)的抽象层主要包括三个接口,分别是PlatformTransactionManagerTransactionDefinitionTransactionStatus,它们位于org.springframework.transaction包中,三者间的关系如下:

img

  TransactionDefinition用于描述事务的隔离级别、超时时间、是否为只读事务和事务传播规则等控制事务具体行为的事务属性,这些事务实现可以通过XML配置或注解描述提供,也可以通过手工编程的方式设置。

  PlatformTransactionManager根据TransactionDefinition提供的事务属性配置信息创建事务,并用TransactionStatus描述这个激活事务的状态。下面分别描述这些接口:

TransactionDefinition

  该接口定义了Spring兼容的事务属性,这些属性对事务管理控制的若干方面进行配置。

  • 事务隔离:当前事务和其他事务的隔离程度。
  • 事务传播:通常在一个事务中执行的所有代码都会允许在同一事务上下文中。
  • 事务超时:事务在超时前能允许多久,超过时间后,事务被回滚。
  • 只读状态:只读事务不修改任何数据,资源事务管理者可以针对可读事务应用一些优化措施,提高运行性能。   

  Spirng允许通过XML或注解元数据的方式为一个有事务要求的服务类方法配置事务属性,这些信息作为Spring事务管理框架的“输入”,Spring将自动按事务属性信息的指示,为目标方法提供相应的事务支持。

TransactionStatus

  该接口代表一个事务的具体运行状态。事务管理者可以通过该接口获取事务运行期的状态信息,也可以通过该接口间接地回滚事务,它相比于抛出异常时回滚事务的方式更具可控性。

PlatformTransactionManager

  通过JDBC的事务管理知识可以知道,事务只能被提交或回滚(或回滚到某个保存点后提交),而该接口很好地描述了事务管理这个概念,解释见代码注释:

1
2
3
4
5
6
7
8
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中进行如下配置即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?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框架配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?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获取对应线程绑定资源的工具类,如下图:

img

这些工具类都提供了静态的方法,通过这些方法可以获取和当前线程绑定的资源。

事务传播行为

  当我们调用一个基于 Spring 的 Service 接口方法(如UserServicesaveUser()方法)时,它将运行于 Spring 管理的事务环境中,Service 接口方法可能会在内部调用其他的 Service 接口方法以共同完成一个完整的业务操作,因此就会产生服务接口方法嵌套调用的情况,Spring 通过事务传播行为控制当前的事务如何传播到被签到调用的目标服务接口方法中。
  Spring在TransactionDefinition接口中规定了以下 7 种类型的事务传播行为:

事务传播行为

Spring 事务管理的两种方式

  • 编程式事务管理:通过编写代码实现的事务管理,包括定义事务的开始、正常执行后的事务提交和异常时的事务回滚。
  • 声明式事务管理:通过 AOP 技术实现的事务管理,主要思想是将事务作为一个“切面”代码单独编写,然后通过 AOP 技术将事务管理的“切面”植入到业务目标类中。

  声明式事务管理的最大优点在于开发者无需通过编程的方式来管理事务,只需在配置文件中进行相关的事务规则声明,就可以将事务应用到业务逻辑中。这使得开发人员可以更加专注于核心业务逻辑代码的编写,在一定程度上减少了工作量,提高了开发效率,所以在实际开发中,通常都推荐使用声明式事务管理

编程式的事务管理

1
2
3
4
5
6
7
8
@Autowired
protected TransactionTemplate transactionTemplate;

transactionTemplate.execute((action) -> {
// 处理业务逻辑,如果想要提前回滚,可以手动抛异常,特别灵活

return Boolean.TRUE;
});

声明式事务管理

  大多数 Spring 用户选择声明式事务管理的功能,这种方式对代码的侵入性最小,可以让事务管理代码完全从业务代码中移除,非常复合非侵入式轻量级容器的理念。
  Spring 的声明式事务管理是通过 Spring AOP 实现的,通过事务的声明式信息,Spring 负责将事务管理增强逻辑东塔织入业务方法的相应连接点中。这些逻辑包括获取线程绑定资源、开始事务、提交/回滚事务、进行异常转换和处理等工作。
  在 Spring 早期版本中,用户必须通过TransactionProxyFactoryBean代理类对需要事务管理的业务类进行代理,以便实施事务功能的增强。现在我们可以通过aop/tx命名空间声明事务,因此代理类实施声明式事务的方法基本不再使用。当然,了解TransactionProxyFactoryBean有助于我们更直观地理解 Spring 实施声明式事务的内部工作原理,通过一个例子来了解:

  1.创建一个业务 Bean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@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个方法,我们希望addTopicupdateForum()方法拥有写事务的能力,而其他两个方法只需要有读事务的能力就可以了。

  2.使用TransactionProxyFactoryBean配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?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为前缀的方法,它匹配BbtForumgetForum(int forumId)getForumNum()方法;而key="*"代表匹配BbtForum接口的所有方法。
  <prop>内的值为事务属性信息,配置格式如下:

事务属性设置格式

基于 XML 配置的声明式事务(了解)

  使用TransactionProxyFactoryBean代理工厂类为业务类添加事务支持缺点是配置过于繁琐,所以 Spring 后来在配置中添加了一个 tx 命名空间,在配置文件中以明确结构化的方式定义事务属性,大大提高了配置事务属性的便利性,<tx:advice>标签用法格式如下:

img

  我们用tx和aop命名空间对前面基于FactoryBean的事务配置方式进行替换,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?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>元素用于的属性如下:

img

  如果需要为不同的业务类Bean应用不同的事务管理风格,则可以在<aop:config>中定义另外多套事务切面,具体的配置方法在现有基础上演绎即可。

基于注解的声明式事务(常用)

  除了基于 XML 的事务配置,Spring 还提供了基于注解的事务配置,即通过@Transactional对需要事务增强的 Bean 接口、实现类或方法进行标注;在容器中配置基于注解的事务增强驱动,即可启用基于注解的声明式事务。现在项目中基本都采用这种配置。下面为配置步骤:

  1.在需要事务管理的业务Bean前加上一个@Transactional注解:

1
2
3
4
5
@Service
@Transactional
public class BbtForum{
.......
}

  2.在配置文件加入标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?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.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,则可以进一步简化配置为:

1
<tx:annotation-driven/>

@Transactional 注解属性

  和XML配置方式一样,该注解也拥有一组普适性很强的默认事务属性,往往可以直接使用这些默认属性,具体如下:

  • 事务传播行为:PROPAGATION_REQUIRED
  • 事务隔离级别:ISOLATION_DEFAULT
  • 读写事务属性:读/写事务。
  • 超时时间:依赖于底层的事务系统的默认值。
  • 回滚设置:任何运行期异常引发回滚,任何检查型异常不会引发回滚。

  这些默认设置在大多数情况下都适用,当然,Spring 也运行通过手工设定属性值覆盖默认值。

img

@Transactional 注解的两个位置

  @Transactional注解既可在类上使用亦可在类方法上使用:

  • 在类上:表示事务的设置对该类中所有方法都起作用;
  • 在类方法上:表示事务的设置只对该方法有效,

注意哦:如果即在类上使用该注解,又在类方法上使用该注解,则类方法上的注解会覆盖类上的注解。

Spring 事务工作原理——ThreadLocal

  从前面的文章我们知道,Spring 通过各类模板类降低了开发者使用各种数据持久化技术的难度。这些模板类都是线程安全的,也就是说,多个 DAO 可以复用同一个模板实例而不会发生冲突。使用模板类访问底层数据,根据持久化技术的不同,模板类需要绑定数据连接或会话的资源。但这些资源本身是非线程安全的,也就是说它们不能在同一时刻被多个线程共享。
  虽然模板类通过资源获取数据连接或会话,但资源池本身解决的是数据连接或会话的缓存问题,并非数据连接或会话的线程安全问题。
  按照传统经验,如果某个对象是非线程安全的,在多线程环境下,对对象的访问必须采用synchronized进行线程同步。但模板类并未采用线程同步机制,因为线程同步会降低并发性,影响系统性能。此外,通过代码同步解决线程安全的挑战性很大,可能会增加几倍的实现难度。
  那么,模板类如何在无须线程同步的情况下解决线程安全的难题呢?
  答案就是ThreadLocal

  对 Spring 而言,ThreadLocal在管理 request 作用域的 Bean、事务管理、任务调度、AOP等模块种都发挥着重要的作用。
  那么什么是ThreadLocal

什么是 ThreadLocal?

  ThreadLocal,顾名思义,它不是一个线程,而是保存线程本地化对象的容器
  当运行于多线程环境的某个对象使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程分配一个独立的变量副本。所以每个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的变量副本。从线程的角度看,这个变量就像线程专用的本地变量。
  可以总结为一句话:ThreadLocal 的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度,即 ThreadLocal 解决了线程上下文中的变量传递问题

ThreadLocal VS synchronized

  它们都是为了解决多线程中相同变量的访问冲突问题。那么,ThreadLocal有什么优势?
  在synchronized同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序缜密地分析什么时候对变量进行读/写,什么时候需要锁定某个对象、什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。
  而ThreadLocal从另一个角度来解决多线程的并发访问。ThreadLocal为每个线程提供了一个独立的变量副本,从而隔离了多个线程对访问数据的冲突问题。因为每个线程都拥有自己的变量副本,因而也就没有必要对该变量进行同步。ThreadLocal提供了线程安全的对象封装,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal

Spring 中的 ThreadLocal

  我们知道在一般情况下,只有无状态的 Bean 才可以在多线程环境下共享,在 Spring 中, 绝大部分 Bean 都可以声明为 singleton 作用域。就是因为 Spring 对一些 Bean(如RequestContextHolder、 TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全状态采用 ThreadLocal 进行处理,让它们也成为线程安全的状态,因为有状态的 Bean 就可以在多线程中共享了。

小节

  对于多线程资源共享问题:

  • synchronized同步机制采用了“以时间换空间”的方式:访问串行化、对象共享化;
  • ThreadLocal采用了“以空间换时间”的方式:访问并行化,对象独享化。

  前者仅提供一份变量,让不同的线程排队访问;而后者为每个线程都提供了一份变量,因此可以同时访问而互不影响。
  举个例子在寝室烧水的例子:

  对synchronized而言,只有一个水壶,A 同学用水壶烧水时(水壶被加锁),B 同学只能等待水开且被 A 用掉后(水壶解锁),才能使用水壶(再次加锁),其他同学还得等待,循环往复。。。
  对ThreadLocal而言,现在有多个相同的水壶,A 同学用水壶 a(水壶 a 被加锁),并不影响 B 同学使用水壶 b .

Spring 事务使用须知

  作为 Java 程序员,在日常开发通过 Spring 操作数据库过程中,我们都想借助事务来保证数据最终正确,但需要注意的两个问题是:

  • ① 事务并不总是生效
  • ② 事务就算生效了,也不一定会回滚

  那么,为什么会出现这两个问题呢,下面,我们研究一下。

事务不生效

  要想让事务在 Spring 中生效,需要注意:

  • 两个前提条件
  • 两个使用技巧

两个前提条件

存储引擎支持事务

  并非所有的存储引擎都支持事务,因此,若想使用事务,就必须选用一种支持事务处理的存储引擎,如 InnoDB 或 Falcon ,这可以在建库建表时指定。

Spring 中开启事务

  在 Spring 中,使用事务的一个前提是——开启事务,如果忘了开启,事务肯定是不会生效的。

  在 SpringBoot 框架中,是通过DataSourceTransactionManagerAutoConfiguration类,默认开启了事务。

  但是,若你使用的还是传统的 Spring XML 方式配置项目,则需要在相关文件中手动配置开启事务相关参数。

两个使用技巧

Spring 动态代理必须生效

  如果你看过 Spring 事务的源码,就会知道 Spring 事务底层借助于 AOP,也就是通过 JDK 动态代理或者 Cglib,帮我们生成了代理类,在代理类中实现事务功能。

  因此,如果代理类没生成或者代理类生成的有问题,事务功能就无法开启。

  那么,什么情况下会造成这种现象呢?

类未被 Spring 管理

  Spring 生成代理对象的前提是,要求事务所在类要必须被 Spring 管理,即相关类需要成为 Bean。

  因此,若相关类没有成为 Spring 的 Bean,则无法生成代理类。

使用了错误的访问权限修饰符

  Spring 生成代理对象要求被代理方法必须是public的。

  换而言之,若我们写的事务方法访问权限不是public,而是privatedefaultprotected的话,则 Spring 无法生成代理对象,因此也就不会提供事务功能。

方法用 final 修饰

  如果某个方法用 final 修饰了,那么在它生成的代理类中,就无法重写该方法,那么也无法添加事务功能。

普通方法的内部调用

  在同一个类中的方法(此方法为没有使用@Transactional注解标识)直接内部调用(隐式或直接 this 调用),由于代理类未生成,那么事务功能也就没有了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Servcie
public class ServiceTest {
// 无事务功能
// @Transactional
public void outerMethod1() {
saveStudent();
saveScore();
}

// 有事务功能
@Transactional
public void outerMethod2() {
saveStudent();
saveScore();
}

@Transactional
public void saveStudent() {
// do something
}

@Transactional
public void saveScore() {
// do something
}
}

  那么问题来了,如果有些场景,确实想在同一个类的某个方法中,调用它自己的另外一个方法,该怎么办呢?

  通常做法是,在相关类中使用AopContext.currentProxy()方法获取代理对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Servcie
public class ServiceTest {
// 有事务功能
public void outerMethod1() {
((ServiceTest)AopContext.currentProxy()).saveStudent();
}

@Transactional
public void saveStudent() {
// do something
}

}

  注意哦,不要被 AOP 误导了,即使下面代码中@Transactional标识的方法内部调用的方法没有用@Transactional修饰,使用了private修饰,它们也是属于同一个事务上下文中的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Servcie
public class ServiceTest {

// 有事务功能
@Transactional
public void outerMethod2() {
saveStudent();
saveScore();
}

private void saveStudent() {
// do something
}

private void saveScore() {
// do something
}
}

多线程调用

  在实际项目开发中,多线程经常使用。

  那么,如果 Spring 事务用在多线程场景中,会有问题吗?

  对于同一个事务而言,其实是指同一个数据库连接,只有拥有同一个数据库连接操作的数据才能同时提交和回滚。

  如果是不同的线程,拿到的数据库连接肯定是不一样的,所以是不同的事务。

  因此,多线程下同一个事务此说法本身就是错的。

事务未回滚

  即使我们通过上述注意事项进行合理的代码编写,事务也开启成功了,但你会发现有时候,事务并没有进行回滚操作。

  为什么会出现这种现象呢?

  具体而言,这由 Spring 的事务传播类型来决定。

事务传播类型

  事务传播类型,指的是事务与事务之间的交互策略。例如:在事务方法 A 中调用事务方法 B,当事务方法 B 失败回滚时,事务方法 A 应该如何操作?这就是事务传播类型。

  Spring 事务中定义了 7 种事务传播类型,分别是:

类型 说明 支持
① REQUIRED 若当前没有事务,就新建一个事务,若已经存在一个事务中,加入到这个事务中 支持当前事务
② SUPPORTS 支持当前事务,若当前没有事务,就以非事务方式执行 支持当前事务
③ MANDATORY 使用当前的事务,若当前没有事务,就抛出异常 支持当前事务
④ REQUIRES NEW 新建事务,若当前存在事务,把当前事务挂起 不支持当前事务
⑤ NOT SUPPORTED 以非事务方式执行操作,若当前存在事务,就把当前事务挂起 不支持当前事务
⑥ NEVER 以非事务方式执行,若当前存在事务,则抛出异常 不支持当前事务
⑦ NESTED 若当前存在事务,则在嵌套事务内执行。若当前没有事务,则执行与 ① 类似操作 嵌套事务

  其中最常用的只有 3 种,即:REQUIRED、REQUIRES_NEW、NESTED

  针对事务传播类型,我们要弄明白的几个点:

  • 是否新建多个事务,即同时存在父子事务
  • 父事务与子事务的关系,子事务是否会启动一个新的事务?
  • 子事务异常时,父事务是否会回滚?
  • 父事务异常时,子事务是否会回滚?
  • 父事务捕捉子事务异常后,父事务是否还会回滚?

REQUIRED

  REQUIRED 是 Spring 默认的事务传播类型,该传播类型的特点是:当前方法存在事务时,子方法加入该事务。此时父子方法共用一个事务,无论父子方法哪个发生异常回滚,整个事务都回滚。即使父方法捕捉了异常,也是会回滚。而当前方法不存在事务时,子方法新建一个事务。

  为了验证 REQUIRED 事务传播类型的特点,我们来做几个测试。

  还是上面 mA 和 mB 的例子。

  当 mA 不开启事务,mB 开启事务,这时候 mB 就是独立的事务,而 mA 并不在事务之中。因此当 mB 发生异常回滚时,mA 中的内容就不会被回滚。用如下的代码就可以验证我们所说的。

1
2
3
4
5
6
7
8
9
10
11
12
public void mA(){
System.out.println("mA");
tableService.insertTableA(new TableEntity());
transactionSB.mB();
}

@Transactional
public void mB(){
System.out.println("mB");
tableService.insertTableB(new TableEntity());
throw new RuntimeException();
}

  最终的结果是:tablea 插入了数据,tableb 没有插入数据,符合了我们的猜想。

  当 mA 开启事务,mB 也开启事务。按照我们的结论,此时 mB 会加入 mA 的事务。此时,我们验证当父子事务分别回滚时,另外一个事务是否会回滚。

  我们先验证第一个:当父方法事务回滚时,子方法事务是否会回滚?

1
2
3
4
5
6
7
8
9
10
11
@Transactional
public void mA(){
tableService.insertTableA(new TableEntity());
transactionSB.mB();
throw new RuntimeException();
}

@Transactional
public void mB(){
tableService.insertTableB(new TableEntity());
}

  结果是:talbea 和 tableb 都没有插入数据,即:父事务回滚时,子事务也回滚了。

  我们继续验证第二个:当子方法事务回滚时,父方法事务是否会回滚?

1
2
3
4
5
6
7
8
9
10
11
@Transactional
public void mA(){
tableService.insertTableA(new TableEntity());
transactionSB.mB();
}

@Transactional
public void mB(){
tableService.insertTableB(new TableEntity());
throw new RuntimeException();
}

  结果是:talbea 和 tableb 都没有插入数据,即:子事务回滚时,父事务也回滚了。

  我们继续验证第三个:当字方法事务回滚时,父方法捕捉了异常,父方法事务是否会回滚?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Transactional
public void mA() {
tableService.insertTableA(new TableEntity());
try {
transactionSB.mB();
} catch (Exception e) {
System.out.println("mB occur exp.");
}
}

@Transactional
public void mB() {
tableService.insertTableB(new TableEntity());
throw new RuntimeException();
}

  结果是:talbea 和 tableb 都没有插入数据,即:子事务回滚时,父事务也回滚了。所以说,这也进一步验证了我们之前所说的:REQUIRED 传播类型,它是父子方法共用同一个事务的。

REQUIRES_NEW

  REQUIRES_NEW 也是常用的一个传播类型,该传播类型的特点是:无论当前方法是否存在事务,子方法都新建一个事务。此时父子方法的事务时独立的,它们都不会相互影响。但父方法需要注意子方法抛出的异常,避免因子方法抛出异常,而导致父方法回滚。 为了验证 REQUIRES_NEW 事务传播类型的特点,我们来做几个测试。

  首先,我们来验证一下:当父方法事务发生异常时,子方法事务是否会回滚?

1
2
3
4
5
6
7
8
9
10
11
@Transactional
public void mA(){
tableService.insertTableA(new TableEntity());
transactionSB.mB();
throw new RuntimeException();
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void mB(){
tableService.insertTableB(new TableEntity());
}

  结果是:tablea 没有插入数据,tableb 插入了数据,即:父方法事务回滚了,但子方法事务没回滚。这可以证明父子方法的事务是独立的,不相互影响。

  下面,我们来看看:当子方法事务发生异常时,父方法事务是否会回滚?

1
2
3
4
5
6
7
8
9
10
11
@Transactional
public void mA(){
tableService.insertTableA(new TableEntity());
transactionSB.mB();
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void mB(){
tableService.insertTableB(new TableEntity());
throw new RuntimeException();
}

  结果是:tablea 没有插入了数据,tableb 没有插入数据。

  从这个结果来看,貌似是子方法事务回滚,导致父方法事务也回滚了。但我们不是说父子事务都是独立的,不会相互影响么?怎么结果与此相反呢?

  其实是因为子方法抛出了异常,而父方法并没有做异常捕捉,此时父方法同时也抛出异常了,于是 Spring 就会将父方法事务也回滚了。如果我们在父方法中捕捉异常,那么父方法的事务就不会回滚了,修改之后的代码如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Transactional
public void mA(){
tableService.insertTableA(new TableEntity());
// 捕捉异常
try {
transactionSB.mB();
} catch (Exception e) {
e.printStackTrace();
}
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void mB(){
tableService.insertTableB(new TableEntity());
throw new RuntimeException();
}

  结果是:tablea 插入了数据,tableb 没有插入数据。这正符合我们刚刚所说的:父子事务是独立的,并不会相互影响。

  这其实就是我们上面所说的:父方法需要注意子方法抛出的异常,避免因子方法抛出异常,而导致父方法回滚。因为如果执行过程中发生 RuntimeException 异常和 Error 的话,那么 Spring 事务是会自动回滚的。

NESTED

  NESTED 也是常用的一个传播类型,该方法的特性与 REQUIRED 非常相似,其特性是:当前方法存在事务时,子方法加入在嵌套事务执行。当父方法事务回滚时,子方法事务也跟着回滚。当子方法事务发送回滚时,父事务是否回滚取决于是否捕捉了异常。如果捕捉了异常,那么就不回滚,否则回滚。

  可以看到 NESTED 与 REQUIRED 的区别在于:父方法与子方法对于共用事务的描述是不一样的,REQUIRED 说的是共用同一个事务,而 NESTED 说的是在嵌套事务执行。这一个区别的具体体现是:在子方法事务发生异常回滚时,父方法有着不同的反应动作。

  对于 REQUIRED 来说,无论父子方法哪个发生异常,全都会回滚。而 REQUIRED 则是:父方法发生异常回滚时,子方法事务会回滚。而子方法事务发送回滚时,父事务是否回滚取决于是否捕捉了异常。

  为了验证 NESTED 事务传播类型的特点,我们来做几个测试。

  首先,我们来验证一下:当父方法事务发生异常时,子方法事务是否会回滚?

1
2
3
4
5
6
7
8
9
10
11
@Transactional
public void mA() {
tableService.insertTableA(new TableEntity());
transactionSB.mB();
throw new RuntimeException();
}

@Transactional(propagation = Propagation.NESTED)
public void mB() {
tableService.insertTableB(new TableEntity());
}

  结果是:tablea 和 tableb 都没有插入数据,即:父子方法事务都回滚了。这说明父方法发送异常时,子方法事务会回滚。

  接着,我们继续验证一下:当子方法事务发生异常时,如果父方法没有捕捉异常,父方法事务是否会回滚?

1
2
3
4
5
6
7
8
9
10
11
@Transactional
public void mA() {
tableService.insertTableA(new TableEntity());
transactionSB.mB();
}

@Transactional(propagation = Propagation.NESTED)
public void mB() {
tableService.insertTableB(new TableEntity());
throw new RuntimeException();
}

  结果是:tablea 和 tableb 都没有插入数据,即:父子方法事务都回滚了。这说明子方法发送异常回滚时,如果父方法没有捕捉异常,那么父方法事务也会回滚。

  最后,我们验证一下:当子方法事务发生异常时,如果父方法捕捉了异常,父方法事务是否会回滚?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Transactional
public void mA() {
tableService.insertTableA(new TableEntity());
try {
transactionSB.mB();
} catch (Exception e) {

}
}

@Transactional(propagation = Propagation.NESTED)
public void mB() {
tableService.insertTableB(new TableEntity());
throw new RuntimeException();
}

  结果是:tablea 插入了数据,tableb 没有插入数据,即:父方法事务没有回滚,子方法事务回滚了。这说明子方法发送异常回滚时,如果父方法捕捉了异常,那么父方法事务就不会回滚。

小结

  • 定义SA.mA()PROPAGATION_REQUIRED修饰;

  • 定义SB.mB()以表格中三种方式修饰;

  • mA中调用mB;

状态 PROPAGATION_REQUIRED PROPAGATION_REQUIRES_NEW PROPAGATION_NESTED
mA 正常 mB正常 A B 一起提交 B 先提交,A 再提交 A B 一起提交
mA 抛异常 mB 正常 A B 一起回滚 A 回滚,B 正常提交 A B 一起回滚
mA 正常 mB 抛异常 A B 一起回滚 若 A 中捕获 B 的异常并没有继续向上抛异常,则 B 先回滚,A 再正常提交
若 A 未捕获 B 的异常,默认则会将 B 的异常向上抛 则 B 先回滚,A 再回滚
B 先回滚,A 再正常提交
mA 抛异常 mB 抛异常 A B 一起回滚 B 先回滚,A 再回滚 A B 一起回滚

参考

  • 陈雄华 林开雄 文建国. 精通 Spring 4.x 企业应用开发 [M]. 电子工业出版社,2017

文章信息

时间 说明
2019-03-16 初稿
2023-07-02 部分重构
0%