一、概述
日常开发中,事务的应用是一个绕不开的话题,我们经常会在对数据库的操作代码里应用到事务。事务的编写和控制是一些很繁琐,且与业务无关的代码,需要非常小心。实际在开发中并不需要每次应用事务时都自行负责整个事务代码的编写。借助于Spring提供的事务管理功能,我们只需要通过少量的配置,就可以让业务方法拥有事务的特性。受益于spring事务管理优秀的设计,RD可以专注于业务代码的编写,在引入事务管理时,基本上可以做到对于业务代码没有侵入性。Spring事务管理机制帮我们完成了大量背后的工作,我们才可以在事务的应用上,显得如此轻松简单。我们不需要去掌握其实现细节里的每一行代码,但是,如果对这个框架的原理和大体流程能够有所了解,这一方面能够让我们以正确的方式使用,出错了能够便于排查。另一方面,这个框架的一些设计理念和知识,也是值得我们学习的。本文主要分两部分,开始会介绍一下Spring的事务管理的一些基础知识,然后重点围绕@Transactional注解,介绍其可能的失效原因、实现机制、以及遇到的一些线上故障。
二、Spring事务管理
这一部分,主要介绍一下Spring事务管理都提供了哪些功能,以及如何使用。
Spring 事务管理概述
Spring的使用方式有两种,即通常所说的编程式事务和声明式事务。其中,编程式事务是指应用通过使用spring提供的各个事务相关的类,通过编写代码来完成事务的设置,这种的使用门槛较高,使用难度稍大一些,而且不方便,大多数时候我们并不会用到。而声明式事务则指通过配置的形式来引入spring的事务管理,具体的配置方式又可以分为通过XML文件配置和通过注解配置。由于配置方式上手容易,需要配置的内容也不多,尤其是基于注解的配置,已经成了目前引入事务的首选。
Spring 事务属性
Spring的事务有5大属性,可以图示如下:

传播属性
传播属性也称为传播行为,是为了解决当多个事务同时存在的时候,Spring如何处理这些事务。
通俗的讲事务传播就是当一个事务方法调用其它方法时,被调用的方法可以决定如何处理调用方的事务,是抛出异常(NEVER)?还是挂起调用方的事务(NOT_SUPPORTED)?还是被调用方法自己再开启一个事务(REQUIRES_NEW)?同时需要特别说明的是数据库并没有事务传播的概念.
该属性一共有七个可选值,这七个值及对应说明如下:
| 传播行为 | 说明 |
|---|---|
| REQUIRED | 默认的传播级别,若当前存在事务则使用当前事务,若不存在则创建新的事务(若外围方法开启事务,REQUIRED修饰的内部方法会加入到外围方法的事务中,所有REQUIRED修饰的内部方法和外围方法均属于同一事务,只要一个方法回滚,则整个事务均回滚) |
| SUPPORTS | 若当前存在事务则使用当前事务,若不存在事务则不使用事务 |
| MANDATORY | 若当前存在事务则使用当前事务,否则抛出异常 |
| REQUIRES_NEW | 无论当前是否有事务都创建一个新的事务,若原来有事务则将其挂起(若外围方法开启事务,REQUIRES_NEW 修饰的内部方法依然会单独开启独立事务,且与外部方法事务也独立,内部方法之间、内部方法和外部方法事务均相互独立,互不干扰) |
| NOT_SUPPORTED | 不使用事务,若原来有事务则将其挂起 |
| NEVER | 不适用事务,若原来有事务则抛出异常 |
| NESTED | 如果存在当前事务,则在嵌套事务中执行,否则行为类似于 REQUIRED(若外围方法开启事务,NESTED 修饰的内部方法属于外部事务的子事务,外围主事务回滚,子事务一定回滚,而内部子事务可以单独回滚而不影响外围主事务和其他子事务) |
隔离级别
脏读:表示一个事务能够读取另一个事务中还未提交的数据。比如,某个事务尝试插入记录A,此时该事务还未提交,然后另一个事务尝试读取到了记录A。
不可重复读:是指在一个事务内,多次读同一数据。
幻读:指同一个事务内多次查询返回的结果集不一样。比如同一个事务A第一次查询时候有n条记录,但是第二次同等条件下查询却有n+1条记录,这就好像产生了幻觉。发生幻读的原因也是另外一个事务新增或者删除或者修改了第一个事务结果集里面的数据,同一个记录的数据内容被修改了,所有数据行的记录就变多或者变少。
| 隔离级别 | 说明 |
|---|---|
| DEFAULT | 事务的默认隔离级别,使用数据库的默认隔离级别。 |
| READ_UNCOMMITTED | 读未提交,最低的隔离级别。会读到其他事务未提交的数据,存在脏读、不可重复读和幻读的问题 |
| READ_COMMITTED | 读已提交,oracle的默认隔离级别,会读到其他事务已经提交的数据,存在不可重复读和幻读的问题 |
| REPEATABLE_READ | 可重复读,mysql的默认隔离级别,解决了脏读和不可重复读的问题,存在幻读问题 |
| SERIALIZABLE | 序列化,最高的隔离级别,事务被处理为顺序执行,因此性能会有影响 |
是否只读
指对事务性资源进行只读操作。所谓事务性资源就是指那些被事务管理的资源,如数据源、JMS 资源,以及自定义的事务性资源等。如果确定只对事务性资源进行只读操作,那么可以将事务标志为只读的,以提高事务处理的性能。
超时时间
指一个事务所允许的最大超时时间,单位为秒,默认为基础事务的超时时间,若超过指定的时间仍未完成则会回滚。由于仅适用于新启动的事务,所以该属性专门于 REQUIRED 或者REQUIRES_NEW传播级别
回滚规则
如果方法执行发生异常,spring可以将事务进行回滚,也可以继续提交。通过配置回滚规则来进行设定,回滚规则允许我们对于某类异常进行精准的回滚与否的配置。
| 回滚规则 | 说明 |
|---|---|
| rollbackFor | 用于设置需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,则事务回滚 |
| rollbackForClassName | 用于设置需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,则进行事务回滚。 |
| noRollbackFor | 用于设置不需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,不进行事务回滚。 |
| noRollbackForClassName | 用于设置不需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,不进行事务回滚。 |
三、@Transactional注解
1.作用范围
@Transactional 可以作用在接口、类、类方法
作用于类:@Transactional 注解放在类上时,表示该类的所有public方法都配置相同的事务属性信息。
作用于方法:当类配置了@Transactional,方法也配置了@Transactional,方法的事务会覆盖类的事务信息。
作用于接口:不推荐这种使用方法,因为一旦标注在Interface上并且配置了Spring AOP 使用CGLib动态代理,将会导致@Transactional注解失效。
2.属性定义
@Transactional属性定义如下:
public @interface Transactional {
@AliasFor("transactionManager")
String value() default ""; // transactionManager 的别名
@AliasFor("value")
String transactionManager() default ""; //value的别名
Propagation propagation() default Propagation.REQUIRED; //传播行为
Isolation isolation() default Isolation.DEFAULT; //隔离级别,数据库默认
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;//超时时间,数据库默认
boolean readOnly() default false; //是否为只读事务,默认非只读
Class<? extends Throwable>[] rollbackFor() default {}; //需要回滚的异常类
String[] rollbackForClassName() default {}; //需要回滚的异常类名
Class<? extends Throwable>[] noRollbackFor() default {};//不回滚的异常类
String[] noRollbackForClassName() default {};//不回滚的异常类名
}
3.代码配置
<!--1. 定义数据源-->
<bean id="dataSource" class="xxx.DataSource" />
<!--2. 定义事务管理器 -->
<bean name="transactionManager"class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<!--3. 配置注解驱动 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
@Service
public class UserServiceImpl implements UserService {
@Transactional
public void update(UserDO user){
//....
}
}
4.常见坑点
@Transactional虽然给RD编程时带来了极大的便利,但在使用@Transactional注解时也要特别注意下面2类问题,否则会引入线上问题
一、作用范围
@Transactional注解的作用范围可以看出其最小粒度是作用在方法上。若需要给局部代码增加事务的话,需要独立出来作为一个方法。而且迭代过程,没有注意到方法是被事务嵌套的,那么就极有可能会在方法中加入一些如: RPC调用、消息发送、缓存更新、文件写入等耗时操作。那这些操作如果被包含在事务中,会有哪些问题呢?
1、该类操作是无法回滚的,可能会导致数据不一致。如:RPC 调用成功,但是本地事务回滚(需要引入分布式事务解决)
2、事务中引入远程调用(或复杂查询),会导致事务耗时增长。导致数据库连接一直被占用,进而导致数据库连接池耗尽
因此在@Transactional注解不建议加在偏流程编排类的方法内,避免后续研发同学不经意间引入了RPC调用、复杂查询、跨库操作等代码,进而导致生产故障
二、事务失效
失效场景集一:代理不生效
Spring中对注解的解析都是基于代理的,如果目标方法无法被Spring代理到,那么它将无法被Spring进行事务管理。Spring生成代理的方式有两种:
1、基于接口的JDK动态代理,要求目标代理类需要实现一个接口才能被代理
2、基于实现目标类子类的CGLIB代理。Spring在2.0之前,目标类如果实现了接口,则使用JDK动态代理方式,否则通过CGLIB子类的方式生成代理。而在2.0版本之后,如果不在配置文件中显示的指定spring.aop.proxy-tartget-class的值,默认情况下生成代理的方式为CGLIB.
顺着代理的思路,我们来看看哪些情况会因为代理不生效导致事务管控失败。
(1)将注解标注在接口方法上
@Transactional是支持标注在方法与类上的。一旦标注在接口上,对应接口实现类的代理方式如果是CGLIB,将通过生成子类的方式生成目标类的代理,将无法解析到@Transactional,从而事务失效。这种错误我们还是犯得比较少的,基本上我们都会将注解标注在接口的实现类方法上。
(2)被final、static关键字修饰的类或方法
CGLIB是通过生成目标类子类的方式生成代理类的,被final、static修饰后,无法继承父类与父类的方法。
(3)类内部调用调用类内部@Transactional标注的方法
错误示例:

解决思路:
1)新建NService,并将方法迁移到NService
2)当前类注入自己,调用insertTestInnerInvoke时通过注入的TestService调用
3)通过AopContext.currentProxy()获取代理对象(与方式同一原理)
(4)当前类没有被Spring管理
遇到的案例:领域对象是new出来的,没有被Spring管理。但充血模型中对象的行为使用了@Transactional注解
错误示例:

失效场景集二:框架或底层不支持的功能
(1) 标注方法修饰符为非 public 时
标有@Transactional的任意方法上打个断点,在IDEA内能看到事务切面点,然后一路Debug,如下图所示
本文介绍了Spring事务管理的基础知识,包括编程式和声明式事务,以及事务的五个属性。接着详细探讨了@Transactional注解,包括其作用范围、属性定义、代码配置和常见问题。文章指出,注解的使用需要注意事务的传播行为、回滚规则,以及可能导致事务失效的场景,如代理不生效、多线程调用、数据库引擎不支持事务等。最后,深入分析了@Transactional注解的解析和执行流程,解释了事务如何在方法执行过程中进行管理。
最低0.47元/天 解锁文章
2704

被折叠的 条评论
为什么被折叠?



