【Spring】Spring5 事务
Spring 事务管理介绍
事务需要添加到 JavaEE 三层结构里面 Service 层(业务逻辑层)。在 Spring 进行事务管理操作有两种方式:编程式事务管理、声明式事务管理(推荐使用):
- 编程式事务(需要手动调用事务管理器包裹业务代码进行提交回滚)使用
TransactionTemplate
或者直接使用底层的PlatformTransactionManager
。对于编程式事务管理,Spring推荐使用TransactionTemplate
。 - 声明式事务(只用声明一下注解就可以)是建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明(或通过基于
@Transactional
注解的方式),便可以将事务规则应用到业务逻辑中。
显然声明式事务管理要优于编程式事务管理,这正是Spring倡导的非侵入式的开发方式。声明式事务管理使业务代码不受污染,一个普通的POJO对象,只要加上注解就可以获得完全的事务支持。和编程式事务相比,声明式事务唯一不足地方是,它的最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。但是即便有这样的需求,也存在很多变通的方法,比如,可以将需要进行事务管理的代码块独立为方法等等。
Spring事务原理与AOP原理十分相似,其详细的源码分析见【Spring】Spring5 事务源码分析。
声明式事务——基于注解方式
- 导入相关依赖:数据源、数据库驱动、SpringJDBC模块
1 | <dependency> |
- 配置数据源、JdbcTemplate操作数据库(Spring提供的简化数据库操作的工具)
- 添加 @EnableTransactionManagement 注解开启基于注解的事务管理功能
- 配置事务管理器来控制事务(事务管理器操作数据源,进行事务管理)
1 |
|
- 在类或方法上添加 @Transactional() 注解表明该方法需要添加事务
- 添加到类上,这个类里面所有的方法都添加事务
- 添加到方法上,只有这个方法添加事务
1 |
|
其中,可以在xml中配置数据库并开启事务管理器:
1 | <!-- 1、在 spring 配置文件,开启事务注解,引入名称空间!--> |
1 |
|
声明式事务——基于xml方式
在 Spring 配置文件中进行配置:
- 第一步:配置事务管理器
- 第二步:配置通知
- 第三步:配置切入点和切面
1 | <!--1 创建事务管理器--> |
事务细节参数
read-only
:设置事务为只读事务,不需要增删改操作。可以提高查询速度。timeout
:超时,事务超出指定执行时长后自动终止并回滚。isolation
:设置隔离级别
运行时异常(非检查异常)发生时默认回滚,编译时异常(检查异常)默认不回滚
rollBackFor
:可以让原来默认不回滚的异常回滚noRollBackFor
:可以让原来默认回滚的异常不回滚
事务和线程的关系
当一个新的事务创建时,就会被绑定到当前线程上。
TransactionAspectSupport类中的ThreadLocal<TransactionInfo>
在当前线程保存了一个事务的信息TransactionInfo:
该线程会伴随着这个事务整个生命周期,直到事务提交、回滚或挂起(临时解绑)时该线程才会取消与该事务的绑定。
同时一个线程只能绑定一个事务,若当前线程原本正绑定的事务还未执行完毕就被新的事务所挂起,则该线程与该事务进行临时解绑,并绑定到新创建的事务上;直到新建的事务提交或回滚后,该线程才会结束与该新建事务的绑定,再次重新绑定之前的事务。
上述过程实现的原理为使用链表结构:创建一张TransactionInfo
链表,将新创建的事务TransactionInfo
链接到旧的事务TransactionInfo
的尾部,待新事务执行完毕后再指回旧的事务TransactionInfo
:
当新创建的事务结束时恢复旧的事务状态:
什么是事务挂起,如何实现挂起
对事务的配置在Spring内部会被封装成TransactionInfo,线程绑定了事务,自然也绑定了事务相关的TransactionInfo。挂起事务时,把TransactionInfo取出临时存储,等待执行完成后,把之前临时存储的TransactionInfo重新绑定到该线程上。
关于事务挂起的举例:(某事务挂起之后,任何操作都不在该事务的控制之下)
例如: 方法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 |
|
1 |
|
使用过上述案例进行实验,1代表插入成功,2代表插入失败:
- 场景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()
获取该类的对象时,将获取到该类的动态代理对象,而非原始对象。此时在调用该代理对象accountProxy
的addAccount()
时,将有事务包裹。
而若不调用该代理对象的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创建该对象。
编程式事务使用示例:
可利用TransactionAspectSupport获取当前线程方法栈中的事务状态,在不同的事务中该状态对象不同:
1 | TransactionStatus status = TransactionAspectSupport.currentTransactionStatus(); |
声明式事务原理
@EnableTransactionManagement 注解向容器中添加AutoProxyRegistrar和ProxyTransactionManagementConfiguration组件,二者作用分别为:
- AutoProxyRegistrar:类似于AOP中的AspectJAutoProxyRegistrar,用于向容器中注册InfrastructureAdvisorAutoProxyCreator组件(类似于AOP里的自动代理器,一种后置处理器)来为普通组件进行代理包装,创建代理对象
- ProxyTransactionManagementConfiguration:用于注册事务增强器,该增强器内设置有事务拦截器,将在代理对象执行目标方法时进行拦截,并调用其
invoke()
方法,由事务管理器控制事务的提交与回滚。
Spring事务原理与AOP原理十分相似,都包含有后置处理器和拦截器思想,在组件创建后包装出代理对象、在代理对象执行目标方法时进行拦截,使用事务管理器控制事务的提交与回滚。
详细的源码分析见文章【Spring】Spring5 事务源码分析。