【Spring】Spring5 事务

img

Spring 事务管理介绍

事务需要添加到 JavaEE 三层结构里面 Service 层(业务逻辑层)。在 Spring 进行事务管理操作有两种方式:编程式事务管理、声明式事务管理(推荐使用):

  • 编程式事务(需要手动调用事务管理器包裹业务代码进行提交回滚)使用TransactionTemplate或者直接使用底层的PlatformTransactionManager。对于编程式事务管理,Spring推荐使用TransactionTemplate
  • 声明式事务(只用声明一下注解就可以)是建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明(或通过基于@Transactional注解的方式),便可以将事务规则应用到业务逻辑中。

显然声明式事务管理要优于编程式事务管理,这正是Spring倡导的非侵入式的开发方式。声明式事务管理使业务代码不受污染,一个普通的POJO对象,只要加上注解就可以获得完全的事务支持。和编程式事务相比,声明式事务唯一不足地方是,它的最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。但是即便有这样的需求,也存在很多变通的方法,比如,可以将需要进行事务管理的代码块独立为方法等等。

Spring事务原理与AOP原理十分相似,其详细的源码分析见【Spring】Spring5 事务源码分析

声明式事务——基于注解方式

  1. 导入相关依赖:数据源、数据库驱动、SpringJDBC模块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.12.RELEASE</version>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>

<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.1.2</version>
</dependency>
  1. 配置数据源、JdbcTemplate操作数据库(Spring提供的简化数据库操作的工具)
  2. 添加 @EnableTransactionManagement 注解开启基于注解的事务管理功能
  3. 配置事务管理器来控制事务(事务管理器操作数据源,进行事务管理)
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
@EnableTransactionManagement
@Configuration
public class TxConfig {

// 向容器中注册数据源
@Bean
public DataSource dataSource() throws PropertyVetoException {
ComboPooledDataSource dataSource = new ComboPooledDataSource();
dataSource.setUser("root");
dataSource.setPassword("123456");
dataSource.setDriverClass("com.mysql.jdbc.Driver");
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/test");
return dataSource;
}

@Bean
public JdbcTemplate jdbcTemplate() throws PropertyVetoException {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource());
return jdbcTemplate;
}

// 向容器中注册事务管理器
@Bean
public PlatformTransactionManager transactionManager() throws PropertyVetoException {
return new DataSourceTransactionManager(dataSource());
}

}
  1. 在类或方法上添加 @Transactional() 注解表明该方法需要添加事务
  • 添加到类上,这个类里面所有的方法都添加事务
  • 添加到方法上,只有这个方法添加事务
1
2
3
4
@Transactional(propagation = Propagation.REQUIRED)
public void add(){
update();
}

其中,可以在xml中配置数据库并开启事务管理器:

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
<!-- 1、在 spring 配置文件,开启事务注解,引入名称空间!-->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
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-tx.xsd">

<!--2、在 spring 配置文件配置事务管理器-->
<!--创建事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--注入数据源-->
<property name="dataSource" ref="dataSource"></property>
</bean>

<!--开启事务注解-->
<tx:annotation-driven transactionmanager="transactionManager"></tx:annotation-driven>

<!--3、在 service 类上面(或者 service 类里面方法上面)添加事务注解-->
1
2
3
4
5
@Transactional(propagation = Propagation.REQUIRED)
public void add(){
//调用update方法
update();
}

声明式事务——基于xml方式

在 Spring 配置文件中进行配置:

  • 第一步:配置事务管理器
  • 第二步:配置通知
  • 第三步:配置切入点和切面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!--1 创建事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--注入数据源-->
<property name="dataSource" ref="dataSource"></property>
</bean>

<!--2 配置通知-->
<tx:advice id="txadvice" transaction-manager="transactionManager">
<!--配置事务参数-->
<tx:attributes>
<!--指定哪种规则的方法上面添加事务-->
<tx:method name="accountMoney" propagation="REQUIRED"/>
<tx:method name="account*"/>
</tx:attributes>
</tx:advice>

<!--3 配置切入点和切面-->
<aop:config>
<!--配置切入点-->
<aop:pointcut id="pt" expression="execution(*
com.atguigu.spring5.service.UserService.*(..))"/>
<!--配置切面-->
<aop:advisor advice-ref="txadvice" pointcut-ref="pt"/>
</aop:config>

事务细节参数

  • read-only:设置事务为只读事务,不需要增删改操作。可以提高查询速度。
  • timeout:超时,事务超出指定执行时长后自动终止并回滚。
  • isolation:设置隔离级别

运行时异常(非检查异常)发生时默认回滚,编译时异常(检查异常)默认不回滚

  • rollBackFor:可以让原来默认不回滚的异常回滚
  • noRollBackFor:可以让原来默认回滚的异常不回滚

事务和线程的关系

当一个新的事务创建时,就会被绑定到当前线程上

TransactionAspectSupport类中的ThreadLocal<TransactionInfo>在当前线程保存了一个事务的信息TransactionInfo

image-20210818103221467

该线程会伴随着这个事务整个生命周期,直到事务提交、回滚或挂起(临时解绑)时该线程才会取消与该事务的绑定。

同时一个线程只能绑定一个事务,若当前线程原本正绑定的事务还未执行完毕就被新的事务所挂起,则该线程与该事务进行临时解绑,并绑定到新创建的事务上;直到新建的事务提交或回滚后,该线程才会结束与该新建事务的绑定,再次重新绑定之前的事务。

上述过程实现的原理为使用链表结构:创建一张TransactionInfo链表,将新创建的事务TransactionInfo链接到旧的事务TransactionInfo的尾部,待新事务执行完毕后再指回旧的事务TransactionInfo

image-20210818104726262

image-20210818105411421

当新创建的事务结束时恢复旧的事务状态:

image-20210818105517020

什么是事务挂起,如何实现挂起

对事务的配置在Spring内部会被封装成TransactionInfo,线程绑定了事务,自然也绑定了事务相关的TransactionInfo挂起事务时,把TransactionInfo取出临时存储,等待执行完成后,把之前临时存储的TransactionInfo重新绑定到该线程上

关于事务挂起的举例:(某事务挂起之后,任何操作都不在该事务的控制之下)

https://blog.csdn.net/xiaoshuo566/article/details/83929465

例如: 方法A支持事务,方法B不支持事务,即PROPAGATION_NOT_SUPPORTED。方法A调用方法B:

  • 在方法A开始运行时,系统为它建立Transaction,方法A中对于数据库的处理操作,会在该Transaction的控制之下。
  • 这时,方法A调用方法B,方法A打开的Transaction将挂起,方法B中任何数据库操作,都不在该Transaction的管理之下。
  • 当方法B返回,方法A继续运行,之前的Transaction恢复,后面的数据库操作继续在该Transaction的控制之下提交或回滚。

声明式事务传播特性

事务传播行为就是多个事务方法相互调用时,事务如何在这些方法间传播。Spring支持7种事务传播行为:

  • propagation_required(需要事务,有就加入,没有就新建):如果当前没有事务,就新建一个事务,如果已存在一个事务中,加入到这个事务中,这是最常见的选择。(如果设置为required,则事务的其他属性继承于大事务)好男人。
  • propagation_supports(支持事务,有就加入,没有就非事务):支持当前事务,如果没有当前事务,就以非事务方法执行。懒男人
  • propagation_mandatory(强制使用当前事务,有就加入,没有就抛异常):使用当前事务,如果没有当前事务,就抛出异常。

上述三种类型都支持当前事务,当前如果有事务就加入。

  • propagation_required_new(必须新建事务,当前有就挂起):新建事务,如果当前存在事务,把当前事务挂起。挑剔男
  • propagation_not_supported(不支持事务,当前有就挂起):以非事务方式执行操作,如果当前存在事务,就把当前事务挂起挂起指自己新建一个数据库连接,不再使用之前的数据库连接,在代码中体现为两个方法的connection不相同,详细介绍见上文)。减肥男
  • propagation_never(强制非事务,当前有就抛异常):以非事务方式执行操作,如果当前事务存在则抛出异常IllegalTransactionStateException,该方法内的代码无法运行。神经病

上述三种类型都不支持当前事务,当前如果有事务,要么挂起,要么抛异常。

  • propagation_nested:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与propagation_required类似的操作

Spring 默认的事务传播行为是 PROPAGATION_REQUIRED,它适合于绝大多数的情况。

假设 ServiveX#methodX() 都工作在事务环境下(即都被 Spring 事务增强了),假设程序中存在如下的调用链:Service1#method1()->Service2#method2()->Service3#method3(),那么这 3 个服务类的 3 个方法通过 Spring 的事务传播机制都工作在同一个事务中。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
JdbcTemplate jdbcTemplate;

@Override
@Transactional(propagation = Propagation.REQUIRED_NEW)
public void addAccount(String name, int initMoney) {
String accountId = new SimpleDateFormat("yyyyMMddhhmmss").format(new Date());
jdbc.Template.update("INSERT INTO `account` (accountName, user, money) VALUES(?,?,?)", accountId, name, initMoney);
int i = 1 / 0; // 制造异常
}
}
1
2
3
4
5
6
7
8
@Autowired
AccountService accountService;

@Transactional(propagation = Propagation.REQUIRED)
public void createUser(String name) {
jdbc.Template.update("INSERT INTO `user` (name) VALUES(?)", name);
accountService.addAccount(name, 10000);
}

使用过上述案例进行实验,1代表插入成功,2代表插入失败:

image-20210817144253822

  • 场景1:两个方法都没事务,都是普通方法,因此就算抛出异常,也不影响插入数据
  • 场景2:createUser()没有事务,其仍然能插入数据;addAccount()有事务,其出现异常不能成功插入数据
  • 场景3:createUser()有事务,出现异常后其不能插入数据;addAccount()没有声明事务,但其被createUser()调用,仍然会被事务包裹,出现异常不能成功插入数据。若某个方法包含事务,其调用的其他方法也会包含事务
  • 场景4:addAccount()createUser()的事务挂起,挂起指自己新建一个数据库连接,不再使用之前的数据库连接,在代码中体现为两个方法的connection不相同,详细介绍见上文。因此addAccount()插入成功(因为没有事务,异常也能插入),createUser()插入失败(因为addAccount()抛出了异常,被重新恢复的事务所捕获从而插入失败)
  • 场景5:addAccount()不支持事务,直接抛出IllegalTransactionStateException。所以直接无法运行该方法内插入的语句,所以插入失败;createUser()因为有事务,所以捕获到addAccount()抛出的异常后回滚,插入失败
  • 场景6:见下文场景分析

场景6详细分析:假设Spring IoC中有组件AccountServiceImpl,该组件中的addAccount()方法被@Transactional注解修饰,代表该方法将开启事务。

Spring容器启动时将使用事务后置处理器AutoProxyRegistrar会为该组件创建一个动态代理对象accountProxy(类似于 Spring AOP 原理),该对象将被注入到容器中,其他程序在调用getBean()获取该类的对象时,将获取到该类的动态代理对象,而非原始对象。此时在调用该代理对象accountProxyaddAccount()时,将有事务包裹。

而若不调用该代理对象的addAccount(),而是将该方法直接写在本类中,直接调用本类里的该方法,则不会交由Spring事务管理器拦截,此时的方法和普通方法一样。如果直接 new 一个对象也无法使用事务管理。

结论:只有Spring事务代理对象的方法才能被事务拦截器所拦截。直接调用方法无法被拦截(即使该方法被@Transactional注解修饰)。

@Transactional 失效情况

  • 不通过代理对象的方式调用,而是直接 new 一个目标对象,此时会失效。只有使用 Spring IoC 容器中管理的动态代理对象才可以使得事务生效
  • 若某个方法是 private 修饰的,也会失效。因为 Spring AOP 底层是基于 CGLIB 实现的动态代理,而 CGLIB 是通过字节码生成被代理类的子类的方式(继承)生成代理类的,因此 private 方法无法被代理类访问,也就无法被代理
  • 若抛出的异常不符合 rollbackFor 里定义的异常类型,也会失效(运行时异常(非检查异常)发生时默认回滚,编译时异常(检查异常)默认不回滚)

事务失效情况

  • 抛出检查异常导致事务不能正确回滚。
    • 原因:Spring 默认只会回滚非检查异常。
    • 解法:配置 rollbackFor 属性@Transactional(rollbackFor = Exception.class)
  • 业务方法内自己 try-catch 异常导致事务不能正确回滚
    • 原因:事务通知只有捉到了目标抛出的异常,才能进行后续的回滚处理,如果目标自己处理掉异常,事务通知无法知悉
    • 解法1:异常原样抛出:在 catch 块添加 throw new RuntimeException(e);
    • 解法2:手动设置 TransactionStatus.setRollbackOnly():在 catch 块添加 TransactionInterceptor.currentTransactionStatus().setRollbackOnly();
  • aop 切面顺序导致导致事务不能正确回滚
    • 原因:事务切面优先级最低,但如果自定义的切面优先级和他一样,则还是自定义切面在内层,这时若自定义切面没有正确抛出异常…
    • 解法1、2:同情况2 中的解法:1、2
    • 解法3:调整切面顺序,在 MyAspect 上添加 @Order(Ordered.LOWEST_PRECEDENCE - 1) (不推荐)
  • 非 public 方法导致的事务失效
  • 原因:Spring 为方法创建代理、添加事务通知、前提条件都是该方法是 public 的
  • 解法1:改为 public 方法
  • 解法2:添加 bean 配置如下(不推荐)
  • 父子容器导致的事务失效
    • 原因:子容器扫描范围过大,把未加事务配置的 service 扫描进来
    • 解法1:各扫描各的,不要图简便
    • 解法2:不要用父子容器,所有 bean 放在同一容器
  • 调用本类方法导致传播行为失效
    • 原因:本类方法调用不经过代理,因此无法增强
    • 解法1:依赖注入自己(代理)来调用
    • 解法2:通过 AopContext 拿到代理对象,来调用
    • 解法3:通过 CTW,LTW 实现功能增强
  • @Transactional 没有保证原子行为
    • 原因:事务的原子性仅涵盖 insert、update、delete、select … for update 语句,select 方法并不阻塞
  • @Transactional 方法导致的 synchronized 失效
    • 原因:synchronized 保证的仅是目标方法的原子性,环绕目标方法的还有 commit 等操作,它们并未处于 sync 块内
    • 解法1:synchronized 范围应扩大至代理方法调用
    • 解法2:使用 select … for update 替换 select

编程式事务

编程式事务指需要手动调用事务管理器包裹业务代码进行提交回滚。其需要使用TransactionTemplate或者直接使用底层的PlatformTransactionManager。对于编程式事务管理,Spring推荐使用TransactionTemplate

Spring事务与JDBC事务的关系:上中下三个框分别代表基于AOP的声明式事务、编程式事务、JDBC事务。其中基于AOP的声明式事务原理见文章【Spring】Spring5 事务源码分析

编程式事务中的两个重要类:

  • TransactionManager:事务管理器,用于控制事务的提交和回滚
  • TransactionStatus:事务状态,保存了事务的各种状态,例如保存点等;其可创建保存点并利用其回滚到保存点。使用TransactionManager创建该对象。

image-20210818101801840

编程式事务使用示例:

image-20210818100033643

可利用TransactionAspectSupport获取当前线程方法栈中的事务状态,在不同的事务中该状态对象不同:

1
TransactionStatus status = TransactionAspectSupport.currentTransactionStatus();

声明式事务原理

@EnableTransactionManagement 注解向容器中添加AutoProxyRegistrarProxyTransactionManagementConfiguration组件,二者作用分别为:

  • AutoProxyRegistrar:类似于AOP中的AspectJAutoProxyRegistrar,用于向容器中注册InfrastructureAdvisorAutoProxyCreator组件(类似于AOP里的自动代理器,一种后置处理器)来为普通组件进行代理包装,创建代理对象
  • ProxyTransactionManagementConfiguration:用于注册事务增强器,该增强器内设置有事务拦截器,将在代理对象执行目标方法时进行拦截,并调用其invoke()方法,由事务管理器控制事务的提交与回滚

Spring事务原理与AOP原理十分相似,都包含有后置处理器拦截器思想,在组件创建后包装出代理对象、在代理对象执行目标方法时进行拦截,使用事务管理器控制事务的提交与回滚

详细的源码分析见文章【Spring】Spring5 事务源码分析