1.3 事务进阶
前面我们通过spring事务管理注解@Transactional已经控制了业务层方法的事务。接下来我们要来详细的介绍一下@Transactional事务管理注解的使用细节。我们这里主要介绍@Transactional注解当中的两个常见的属性:
- 异常回滚的属性:rollbackFor :可以指定出现何种异常类型回滚事务。
- 事务传播行为:propagation:指定事务的传播行为
我们先来学习下rollbackFor属性。
1.3.1 rollbackFor
我们在之前编写的业务方法上添加了@Transactional注解,来实现事务管理。
@Transactional
public void delete(Integer id){
//根据部门id删除部门信息
deptMapper.deleteById(id);
//模拟:异常发生
int i = 1/0;
//删除部门下的所有员工信息
empMapper.deleteByDeptId(id);
}
以上业务功能delete()方法在运行时,会引发除0的算数运算异常(运行时异常),出现异常之后,由于我们在方法上加了@Transactional注解进行事务管理,所以发生异常会执行rollback回滚操作,从而保证事务操作前后数据是一致的。
下面我们在做一个测试,我们修改业务功能代码,在模拟异常的位置上直接抛出Exception异常(编译时异常)
@Transactional
public void delete(Integer id) throws Exception {
//根据部门id删除部门信息
deptMapper.deleteById(id);
//模拟:异常发生
if(true){
throw new Exception("出现异常了~~~");
}
//删除部门下的所有员工信息
empMapper.deleteByDeptId(id);
}
说明:在service中向上抛出一个Exception编译时异常之后,由于是controller调用service,所以在controller中要有异常处理代码,此时我们选择在controller中继续把异常向上抛。
@DeleteMapping("/depts/{id}") public Result delete(@PathVariable Integer id) throws Exception { //日志记录 log.info("根据id删除部门"); //调用service层功能 deptService.delete(id); //响应 return Result.success(); }
重新启动服务后测试:
抛出异常之后事务会不会回滚
现有表中数据:
使用postman测试,删除5号部门
发生了Exception异常,但事务依然提交了
dept表中数据:
通过以上测试可以得出一个结论:默认情况下,只有出现RuntimeException(运行时异常)才会回滚事务。
假如我们想让所有的异常都回滚,需要来配置@Transactional注解当中的rollbackFor属性,通过rollbackFor这个属性可以指定出现何种异常类型回滚事务。
@Slf4j
@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;
@Autowired
private EmpMapper empMapper;
@Override
@Transactional(rollbackFor=Exception.class)
public void delete(Integer id){
//根据部门id删除部门信息
deptMapper.deleteById(id);
//模拟:异常发生
int num = id/0;
//删除部门下的所有员工信息
empMapper.deleteByDeptId(id);
}
}
接下来我们重新启动服务,测试删除部门的操作:
控制台日志:执行了删除3号部门的操作, 因为异常又进行了事务回滚
数据表:3号部门没有删除
结论:
- 在Spring的事务管理中,默认只有运行时异常 RuntimeException才会回滚。
- 如果还需要回滚指定类型的异常,可以通过rollbackFor属性来指定。
1.3.3 propagation
1.3.3.1 介绍
我们接着继续学习@Transactional注解当中的第二个属性propagation,这个属性是用来配置事务的传播行为的。
什么是事务的传播行为呢?
- 就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制。
例如:两个事务方法,一个A方法,一个B方法。在这两个方法上都添加了@Transactional注解,就代表这两个方法都具有事务,而在A方法当中又去调用了B方法。
所谓事务的传播行为,指的就是在A方法运行的时候,首先会开启一个事务,在A方法当中又调用了B方法, B方法自身也具有事务,那么B方法在运行的时候,到底是加入到A方法的事务当中来,还是B方法在运行的时候新建一个事务?这个就涉及到了事务的传播行为。
我们要想控制事务的传播行为,在@Transactional注解的后面指定一个属性propagation,通过 propagation 属性来指定事务的传播行为。接下来我们就来介绍一下常见的事务传播行为。
属性值 | 含义 |
---|---|
REQUIRED | 【默认值】需要事务,有则加入,无则创建新事务 |
REQUIRES_NEW | 需要新事务,无论有无,总是创建新事务 |
SUPPORTS | 支持事务,有则加入,无则在无事务状态中运行 |
NOT_SUPPORTED | 不支持事务,在无事务状态下运行,如果当前存在已有事务,则挂起当前事务 |
MANDATORY | 必须有事务,否则抛异常 |
NEVER | 必须没事务,否则抛异常 |
… |
对于这些事务传播行为,我们只需要关注以下两个就可以了:
- REQUIRED(默认值)
- REQUIRES_NEW
1.3.3.2 案例
接下来我们就通过一个案例来演示下事务传播行为propagation属性的使用。
**需求:**解散部门时需要记录操作日志
由于解散部门是一个非常重要而且非常危险的操作,所以在业务当中要求每一次执行解散部门的操作都需要留下痕迹,就是要记录操作日志。而且还要求无论是执行成功了还是执行失败了,都需要留下痕迹。
步骤:
- 执行解散部门的业务:先删除部门,再删除部门下的员工(前面已实现)
- 记录解散部门的日志,到日志表(未实现)
准备工作:
- 创建数据库表 dept_log 日志表:
create table dept_log(
id int auto_increment comment '主键ID' primary key,
create_time datetime null comment '操作时间',
description varchar(300) null comment '操作描述'
)comment '部门操作日志表';
- 引入资料中提供的实体类:DeptLog
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DeptLog {
private Integer id;
private LocalDateTime createTime;
private String description;
}
- 引入资料中提供的Mapper接口:DeptLogMapper
@Mapper
public interface DeptLogMapper {
@Insert("insert into dept_log(create_time,description) values(#{createTime},#{description})")
void insert(DeptLog log);
}
- 引入资料中提供的业务接口:DeptLogService
public interface DeptLogService {
void insert(DeptLog deptLog);
}
- 引入资料中提供的业务实现类:DeptLogServiceImpl
@Service
public class DeptLogServiceImpl implements DeptLogService {
@Autowired
private DeptLogMapper deptLogMapper;
@Transactional //事务传播行为:有事务就加入、没有事务就新建事务
@Override
public void insert(DeptLog deptLog) {
deptLogMapper.insert(deptLog);
}
}
代码实现:
业务实现类:DeptServiceImpl
@Slf4j
@Service
//@Transactional //当前业务实现类中的所有的方法,都添加了spring事务管理机制
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;
@Autowired
private EmpMapper empMapper;
@Autowired
private DeptLogService deptLogService;
//根据部门id,删除部门信息及部门下的所有员工
@Override
@Log
@Transactional(rollbackFor = Exception.class)
public void delete(Integer id) throws Exception {
try {
//根据部门id删除部门信息
deptMapper.deleteById(id);
//模拟:异常
if(true){
throw new Exception("出现异常了~~~");
}
//删除部门下的所有员工信息
empMapper.deleteByDeptId(id);
}finally {
//不论是否有异常,最终都要执行的代码:记录日志
DeptLog deptLog = new DeptLog();
deptLog.setCreateTime(LocalDateTime.now());
deptLog.setDescription("执行了解散部门的操作,此时解散的是"+id+"号部门");
//调用其他业务类中的方法
deptLogService.insert(deptLog);
}
}
//省略其他代码...
}
测试:
重新启动SpringBoot服务,测试删除3号部门后会发生什么?
- 执行了删除3号部门操作
- 执行了插入部门日志操作
- 程序发生Exception异常
- 执行事务回滚(删除、插入操作因为在一个事务范围内,两个操作都会被回滚)
然后在dept_log表中没有记录日志数据
原因分析:
接下来我们就需要来分析一下具体是什么原因导致的日志没有成功的记录。
-
在执行delete操作时开启了一个事务
-
当执行insert操作时,insert设置的事务传播行是默认值REQUIRED,表示有事务就加入,没有则新建事务
-
此时:delete和insert操作使用了同一个事务,同一个事务中的多个操作,要么同时成功,要么同时失败,所以当异常发生时进行事务回滚,就会回滚delete和insert操作
解决方案:
在DeptLogServiceImpl类中insert方法上,添加@Transactional(propagation = Propagation.REQUIRES_NEW)
Propagation.REQUIRES_NEW :不论是否有事务,都创建新事务 ,运行在一个独立的事务中。
@Service
public class DeptLogServiceImpl implements DeptLogService {
@Autowired
private DeptLogMapper deptLogMapper;
@Transactional(propagation = Propagation.REQUIRES_NEW)//事务传播行为:不论是否有事务,都新建事务
@Override
public void insert(DeptLog deptLog) {
deptLogMapper.insert(deptLog);
}
}
重启SpringBoot服务,再次测试删除3号部门:
那此时,DeptServiceImpl中的delete方法运行时,会开启一个事务。 当调用 deptLogService.insert(deptLog) 时,也会创建一个新的事务,那此时,当insert方法运行完毕之后,事务就已经提交了。 即使外部的事务出现异常,内部已经提交的事务,也不会回滚了,因为是两个独立的事务。
到此事务传播行为已演示完成,事务的传播行为我们只需要掌握两个:REQUIRED、REQUIRES_NEW。
REQUIRED :大部分情况下都是用该传播行为即可。
REQUIRES_NEW :当我们不希望事务之间相互影响时,可以使用该传播行为。比如:下订单前需要记录日志,不论订单保存成功与否,都需要保证日志记录能够记录成功。