注意: 研究事务的传播行为的前提是下面两种中的任一种:
- serviceImplA中的事务方法调用serviceImplB中的事务方法(这种情况下Mapper可以同一个);
- 同一个XxxServiceImpl中的方法A调用B,但Mapper不能为同一个;
正如本文章靠下一点反例中说的,同一个serviceImpl的方法间调用,且Mapper还是同一个的,不在事务传播行为研究的范畴哦。
一、事务的传播行为详解
package org.springframework.transaction.annotation;
import org.springframework.transaction.TransactionDefinition;
/**
* Enumeration that represents transaction propagation behaviors for use
* with the {@link Transactional} annotation, corresponding to the
* {@link TransactionDefinition} interface.
*/
public enum Propagation {
REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED),
SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),
MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY),
REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW),
NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED),
NEVER(TransactionDefinition.PROPAGATION_NEVER),
NESTED(TransactionDefinition.PROPAGATION_NESTED);
}
事务的传播行为有七种。下面以【serviceA中的方法A调用serviceB中的方法B】为例说明,方法B上@Transactional()的事务传播行为属性propagation为下面各个值时:
- REQUIRED(默认值):如果A上面有事务注解,就加入该事务;如果A上面没有事务注解,就新建一个事务。
- REQUIRES_NEW:新建事务,如果A存在事务,就把A事务挂起(“挂起”指并列的两个事务,互不影响,而不是嵌套关系)。
- NESTED:如果A存在事务,就在这个事务里再嵌套一个完全独立的事务B,嵌套的事务B可以独立的提交和回滚;如果A没有事务,就和REQUIRED一样。
- MANDATORY:使用A的事务,如果A没有事务,就抛出异常。
- NEVER:以非事务方式执行,如果A存在事务,则抛出异常。
- NOT_SUPPORTED:以非事务方式执行,如果A存在事务,就把A事务挂起。
- SUPPORTS:支持A的事务,如果A没有事务就以非事务方式执行。
只有前两个用的多,最多再加上第三个。但为了面试都得知道。
二、测试代码
2.1 层级代码准备工作
controller层级:
@RestController
@RequestMapping("/transaction/")
public class StudentInfoTransactionController {
@Resource(name = "service1")
private TransactionService transactionService;
/**
* 插入一条学生信息insertStudent
*/
// 访问路径为:http://localhost:8081/transaction/insertStudent
@RequestMapping("/insertStudent")
public int insertStudent() {
int row = transactionService.insertStudent();
return row;
}
}
TransactionServiceImpl1代码:
@Service("service1")
public class TransactionServiceImpl1 implements TransactionService {
@Resource
private StudentInfoMapper studentInfoMapper;
@Autowired
@Qualifier("service2")
private TransactionService transactionService2;
@Override
@Transactional
public int insertStudent() {
StudentInfo student = new StudentInfo();
student.setStuName("测试事务方法1");
student.setPhoneNumber("15536537886");
int row = studentInfoMapper.insertStudent(student);
//调用另外一个service中的方法
transactionService2.insertStudent();
//模拟异常
int i = 1 / 0;
return row;
}
}
TransactionServiceImpl2代码:(它被TransactionServiceImpl1中的方法调用)
@Service("service2")
public class TransactionServiceImpl2 implements TransactionService {
@Resource
private StudentInfoMapper studentInfoMapper;
@Override
@Transactional(propagation = Propagation.REQUIRED)
// @Transactional(propagation = Propagation.REQUIRES_NEW)
// @Transactional(propagation = Propagation.MANDATORY)
// @Transactional(propagation = Propagation.NESTED)
// @Transactional(propagation = Propagation.NEVER)
// @Transactional(propagation = Propagation.NOT_SUPPORTED)
// @Transactional(propagation = Propagation.SUPPORTS)
public int insertStudent() {
StudentInfo student = new StudentInfo();
student.setStuName("测试事务方法2"); student.setPhoneNumber("1*****");
int row = studentInfoMapper.insertStudent(student);
//int i = 1/0;
return row;
}
}
2.2 REQUIRED和REQUIRES_NEW测试
情况一:代码如上,Bean=service1中代码调用Bean=service2中的方法,service2中的方法本身没有异常,service1中的方法在调用service2之后产生了异常。测试结果:数据库没有插入数据。即:service2的事务加入了service1的事务,两者要么同时成功,要么同时失败。日志如下:
在情况一的基础上,如果只把service2注解的传播行为改为REQUIRES_NEW,那么数据库中便把service2的数据插入了。可见,如果是REQUIRES_NEW时,两个事务是独立的,互不影响。日志如下:
情况二:在上面情况一基础上,将service1中的注解@Transactional注释掉,再进行测试结果为:两条数据都插入成功了。service2外层没有事务,所以它会新建一个事务,service2的成功失败只在于service2中是否会出现异常;service1的成功与否当然与service2的事务无关,只与自己有关。日志如下:
在情况一的基础上,如果只把service2注解的传播行为改为REQUIRES_NEW,测试结果为两条数据都插入了,这也是可以想到的。
2.3 NESTED测试
情况一:如果A没有事务,就和REQUIRED一样,会新建一个事务;
情况二:如果A存在事务,就在这个事务里再嵌套一个完全独立的事务B,嵌套的事务B可以独立的提交和回滚。(我测试时,发现是同一个SqlSession,并没有独立提交和回滚,实际的效果是要么都成功,要么都失败!!!)
都失败日志:
都成功日志:
2.4 其他类型的测试
MANDATORY:如果A没有事务时,就抛出异常,但并不会影响到service1中的插入结果。如果A有事务时,使用A的事务。(已测试)
NEVER:如果A存在事务,直接抛异常,service1和service2数据都不会插入。(已测试)
SUPPORTS:支持A的事务,如果A没有事务就以非事务方式执行。
NOT_SUPPORTED:service2以非事务方式执行,即service2有异常也会插入数据;而service1能否插入成功取决于service1和service2这个整体是否有异常。(已测试)
三、反例:A、B两方法在同一个类且是同一个Mapper
测试一:
controller中的代码:
@RestController
@RequestMapping("/transaction/")
public class StudentInfoTransactionController {
@Resource(name = "service1")
private TransactionService transactionService;
/**
* 插入一条学生信息insertStudent
*/
// 访问路径为:http://localhost:8081/transaction/insertStudent
@RequestMapping("/insertStudent")
public int insertStudent() {
int row = transactionService.insertStudent();
return row;
}
}
service层级实现类中的代码:
@Service("service1")
public class TransactionServiceImpl1 implements TransactionService {
@Resource
private StudentInfoMapper studentInfoMapper;
@Override
@Transactional
public int insertStudent() {
StudentInfo student = new StudentInfo();
student.setStuName("测试事务方法1");
student.setPhoneNumber("15536537886");
int row = studentInfoMapper.insertStudent(student);
//调本类中的方法insertStudent222(),这个方法没有异常产生
insertStudent222();
//模拟异常
int i = 1 / 0;
return row;
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public int insertStudent222() {
StudentInfo student = new StudentInfo();
student.setStuName("同一个类中的方法");
student.setPhoneNumber("2....");
int row = studentInfoMapper.insertStudent(student);
return row;
}
}
说明:代码如上所示,在浏览器中输入上面StudentInfoTransactionController类中的url回车,会调用TransactionServiceImpl1类中重写方法insertStudent(),在该方法内部又调用了本类中的被事务注解所标记的insertStudent222()方法,且这两个方法中的是同一个studentInfoMapper(这个前提很重要)。insertStudent222()方法本身没有异常,insertStudent()方法有异常且位置在insertStudent222()方法执行结束后。先说结论:数据库中没有任何数据插入。打印的日志如下:
Creating a new SqlSession Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1cc71227] JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@37fadc8] will be managed by Spring ==> Preparing: insert into student_info( stu_name, phone_number, created_time, updated_time )values( ?, ?, sysdate(), sysdate() ) ==> Parameters: 测试事务方法1(String), 15536537886(String) <== Updates: 1 Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1cc71227] Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1cc71227] from current transaction ==> Preparing: insert into student_info( stu_name, phone_number, created_time, updated_time )values( ?, ?, sysdate(), sysdate() ) ==> Parameters: 同一个类中的方法(String), 2....(String) <== Updates: 1 Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1cc71227] Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1cc71227] Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1cc71227]
结论1:如果A方法(有@Transactional注解)调用了本类中的B方法,且A、B两个方法中的Mapper是同一个的话(如studentMapper),不管B上面是否有注解@Transactional,以及B上面@Transactional的事务传播行为是7中中的哪种,那么B方法的事务会失效,会统统纳入A方法的事务管理,打印的日志也都是上面那样的,一模一样,都是同一个事务型的SqlSession(DefaultSqlSession@1cc71227)。即:这两个方法都没有异常时,两条数据要么都插入成功;两个方法不管哪个有异常时,两条数据都不会插入成功。这种情况不属于事务传播行为研究的范畴哦。【强调:如果A、B两个方法不是同一个Mapper时结果就不一样了】
测试二:
这次测试的大前提还是A、B两方法在同一个类且是同一个Mapper,不同的是,A方法上没有事务注解哦。B上面@Transactional的事务传播行为是7中都试过了,事务会失效,就像根本没有任何事务注解@Transactional一样,即:异常之前的代码都会成功,之后的都会失败。日志都是下面这样的:
四、事务失效情形
Spring事务失效的情形有哪些?(GitCode AI答案)
Spring事务可能在以下情况下失效:
- 数据库不支持事务:如MySQL选择了不支持事务的MyISAM存储引擎。
- 事务方法未被Spring管理:如果事务方法所在的类不在Spring的IOC容器中,该方法不会受到Spring事务控制。
- 方法非public访问:只有public方法的事务注解才会生效。
- 同一类内非事务方法调用事务方法:方法A调用有事务注解的方法B,但A本身无事务注解,那么B的事务不生效。
- 未配置事务管理器:Spring容器中需要有事务管理器才能处理事务。
- 事务传播类型不正确:如果内部方法的传播类型设置为不支持事务的类型,事务将失效。
- 错误地捕获异常:不恰当的异常处理可能导致事务无法正常回滚。
- 标注错误的异常类型:@Transactional注解中指定的异常类型与实际抛出的异常不符,事务回滚可能会失效。
Spring事务失效的8大场景,坑有点多,处理不好真的是坑坑致命!_哔哩哔哩_bilibili