摘要 :本文围绕事务管理展开,先回顾事务基本概念与操作,后深入探讨Spring事务管理。通过具体案例剖析事务管理在实际应用中的问题及解决方案,详细介绍@Transactional
注解及其属性rollbackFor
和propagation
的使用。
关键词 :事务管理;Spring框架;@Transactional
注解;事务传播行为
参考资料 :黑马程序员day13 完整项目请从第10天开始看
一、事务管理概述
1.1 事务回顾
事务是一组不可分割的操作集合,作为一个整体向数据库提交或撤销请求,确保这组操作要么全部成功,要么全部失败。其具体操作包含以下三步:
- 开启事务 :在一组操作开始前执行,指令为
start transaction / begin
。 - 提交事务 :当所有操作成功完成后,使用
commit
指令提交。 - 回滚事务 :若操作过程中出现异常,通过
rollback
指令回滚事务。
1.2 Spring事务管理
1.2.1 案例
以解散部门为例,需求是删除部门信息的同时删除该部门下的所有员工数据。
- 代码实现:
DeptServiceImpl:
java
@Slf4j
@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;
@Autowired
private EmpMapper empMapper;
//根据部门id,删除部门信息及部门下的所有员工
@Override
public void delete(Integer id) {
//根据部门id删除部门信息
deptMapper.deleteById(id);
//删除部门下的所有员工信息
empMapper.deleteByDeptId(id);
}
}
DeptMapper:
java
@Mapper
public interface DeptMapper {
/**
* 根据id删除部门信息
* @param id 部门id
*/
@Delete("delete from dept where id = #{id}")
void deleteById(Integer id);
}
EmpMapper:
java
@Mapper
public interface EmpMapper {
//根据部门id删除部门下所有员工
@Delete("delete from emp where dept_id=#{deptId}")
public int deleteByDeptId(Integer deptId);
}
- 测试情况 :正常运行时,
dept
表和emp
表中的相关数据均被删除。但当在DeptServiceImpl
的delete
方法中添加异常模拟代码(如int i = 1/0;
)后,部门信息被删除,而部门下的员工数据未删除,导致数据不一致。
1.2.2 原因分析
上述问题产生的原因在于,删除部门操作先执行且成功,随后的异常导致后续删除员工数据的操作未执行。要保证数据一致性,需确保解散部门的两个业务操作要么同时成功,要么同时失败,这可通过事务来实现。在Spring框架中,可借助@Transactional
注解简化事务控制代码。
1.2.3 @Transactional
注解
@Transactional
注解用于控制事务,在方法执行前开启事务,执行完毕后提交事务,若执行过程中出现异常则回滚事务。通常在业务层使用(spring的三层业务结构是:表现层cotroller、业务逻辑层service、数据访问层Dao),因为业务层的一个业务功能可能涉及多个数据访问操作,通过在业务层控制事务,可将这些操作纳入同一事务范围。
@Transactional注解可置于方法、类或接口上:
- 方法:仅当前方法由Spring进行事务管理。
- 类:当前类中的所有方法都由Spring管理事务。
- 接口:接口下所有实现类的所有方法均由Spring管理事务。
在DeptServiceImpl
的delete
方法上添加@Transactional
注解后,再次测试,由于异常事务回滚,部门和员工数据均未被删除,保证了数据一致性。同时,可在application.yml
配置文件中开启事务管理日志,以便查看事务相关信息:
yaml
#spring事务管理日志
logging:
level:
org.springframework.jdbc.support.JdbcTransactionManager: debug
1.3 事务进阶
1.3.1 rollbackFor
在业务方法上添加@Transactional
注解实现事务管理时,默认情况下 ,只有RuntimeException
(运行时异常)会触发事务回滚。
例如:
- 当业务方法中抛出除0的算数运算异常(运行时异常)时,事务会回滚。
- 但当抛出
Exception
(编译时异常)时,事务不会回滚。
若希望所有异常都能触发事务回滚,可通过配置@Transactional
注解的rollbackFor
属性指定异常类型。如:
java
@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);
}
}
重新启动服务测试删除部门操作,可发现由于异常事务回滚,部门未被删除。由此可见,在Spring事务管理中,默认仅运行时异常会回滚事务,若要回滚指定类型异常,可通过rollbackFor
属性指定。
PS:rollbackFor参数用来指定哪些异常会触发事务回滚。 rollbackFor = Exception.class 表明所有 Exception 类型的异常都会触发回滚,这其中包含了受检查异常(Checked Exception)。
1.3.3 propagation
1.3.3.1 介绍
propagation
属性用于配置事务的传播行为,即当一个事务方法调用另一个事务方法时,后者应如何进行事务控制。例如,有A
和B
两个事务方法,均添加@Transactional
注解,若A
方法调用B
方法,事务传播行为决定B
方法是加入A
方法的事务,还是新建一个事务。
通过在@Transactional
注解后指定propagation
属性来控制事务传播行为,常见的事务传播行为如下:
属性值 | 含义 |
---|---|
REQUIRED |
【默认值】需要事务,有则加入,无则创建新事务 |
REQUIRES_NEW |
需要新事务,无论有无,总是创建新事务 |
SUPPORTS |
支持事务,有则加入,无则在无事务状态中运行 |
NOT_SUPPORTED |
不支持事务,在无事务状态下运行,如果当前存在已有事务,则挂起当前事务 |
MANDATORY |
必须有事务,否则抛异常 |
NEVER |
必须没事务,否则抛异常 |
实际应用中,重点关注REQUIRED
(默认值)和REQUIRES_NEW
。
1.3.3.2 案例
以解散部门并记录操作日志为例,具体步骤如下:
- 准备工作:
创建数据库表dept_log
:
sql
create table dept_log(
id int auto_increment comment '主键ID' primary key,
create_time datetime null comment '操作时间',
description varchar(300) null comment '操作描述'
)comment '部门操作日志表';
引入实体类DeptLog
、Mapper
接口DeptLogMapper
、业务接口DeptLogService
及业务实现类DeptLogServiceImpl
。
- 代码实现:
java
@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号部门,程序发生
Exception
异常,执行事务回滚,dept_log
表中未记录日志数据。 - 原因分析 :
delete
操作开启一个事务,insert
操作默认事务传播行为为REQUIRED
,即加入delete
操作的事务。由于同一事务中的操作要么同时成功,要么同时失败,异常发生时事务回滚,导致insert
操作也被回滚。
- 测试情况 :重新启动SpringBoot服务,测试删除3号部门,程序发生
- 解决方案 :
在DeptLogServiceImpl
类的insert
方法上添加@Transactional(propagation = Propagation.REQUIRES_NEW)
,表示无论是否存在事务,都创建新事务并独立运行。 重启服务再次测试删除3号部门,insert
方法运行完毕后事务立即提交,即使外部delete
方法所在事务出现异常,已提交的insert
事务也不会回滚。