目录
1. 事务管理
1.1 事务回顾
1.2 Spring事务管理
1.2.1 案例
1.2.2 原因分析
1.2.3 Transactional注解
1.3 事务进阶
1.3.1 rollbackFor属性
编辑结论:
1.3.3 propagation属性
1.3.3.1 介绍
什么是事务的传播行为呢?
1.3.3.2 案例
在数据库阶段我们已学习过事务了,我们讲到:
事务是一组操作的集合,它是一个不可分割的工作单位。事务会把所有的操作作为一个整体,一起向数据库提交或者是撤销操作请求。所以这组操作要么同时成功,要么同时失败。
怎么样来控制这组操作,让这组操作同时成功或同时失败呢?此时就要涉及到事务的具体操作了。
事务的操作主要有三步:
开启事务(一组操作开始前,开启事务):start transaction / begin ;
提交事务(这组操作全部成功后,提交事务):commit ;
回滚事务(中间任何一个操作出现异常,回滚事务):rollback ;
简单的回顾了事务的概念以及事务的基本操作之后,接下来我们看一个事务管理案例:解散部门 (解散部门就是删除部门)
需求:当部门解散了不仅需要把部门信息删除了,还需要把该部门下的员工数据也删除了。
之前写的代码仅仅是删除部门,并没有删除部门下的员工,此时就会造成数据的不完整、不一致。
接下来我们就需要来完善删除部门的功能。
步骤:
根据ID删除部门数据
根据部门ID删除该部门下的员工
DeptController
package com.gch.controller;
/**
部门管理控制器
*/
@Slf4j
@RestController
@RequestMapping("/depts")
public class DeptController {
@Autowired
private DeptService deptService;
/**
* 删除部门 - 事务
* 根据部门id来删除部门以及根据部门id来删除部门下的所有员工
* @DeleteMapping用来指定当前接口的请求方式必须是DELETE请求
* 一个{}就是一个参数占位符,最终id就会替换掉参数占位符
* @return
*/
@DeleteMapping("/{id}")
public Result delete(@PathVariable Integer id) {
log.info("根据id删除部门:{}",id);
// 1.调用service根据部门id删除部门
deptService.delete(id);
// 2.返回统一响应结果
return Result.success();
}
}
DeptService
package com.gch.service;
import com.gch.pojo.Dept;
import java.util.List;
/**
部门业务规则
*/
public interface DeptService {
/**
* 根据部门id来删除对应的部门
* @param id
*/
void delete(Integer id);
}
DeptServiceImpl
package com.gch.service.impl;
/**
部门业务实现类
*/
@Slf4j
@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;
@Autowired
private EmpMapper empMapper;
/**
* 根据部门id来删除对应的部门 - 事务
* @param id
*/
@Override
public void delete(Integer id) {
// 1.根据部门id来删除部门
deptMapper.deleteById(id);
// 2.根据部门id来删除部门下的所有员工信息
empMapper.deleteByDeptId(id);
}
}
DeptMapper
package com.gch.mapper;
/**
部门管理
*/
@Mapper
public interface DeptMapper {
/**
* 根据id删除部门
* @param id => 部门id
*/
@Delete("delete from tlias.dept where id = #{id}")
void deleteById(Integer id);
}
EmpMapper
package com.gch.mapper;
/**
员工管理
*/
@Mapper
public interface EmpMapper {
/**
* 根据部门id来删除部门下的所有员工
* @param deptId 部门id
*/
@Delete("delete from tlias.emp where dept_id = #{deptId}")
void deleteByDeptId(Integer deptId);
}
代码正常情况下,dept表和Em表中的数据已删除。
注意:如果在删除部门的过程中发生了异常,出现部门虽然删除了,但是部门下的员工却没有删除,此时就造成了数据的不完整,不一致。
模拟异常情况:
package com.gch.service.impl;
/**
部门业务实现类
*/
@Slf4j
@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;
@Autowired
private EmpMapper empMapper;
/**
* 根据部门id来删除对应的部门 - 事务
* @param id
*/
@Override
public void delete(Integer id) {
// 1.根据部门id来删除部门
deptMapper.deleteById(id);
// 模拟:异常发生
int i = 1 / 0;
// 2.根据部门id来删除部门下的所有员工信息
empMapper.deleteByDeptId(id);
}
}
原因:
先执行根据id删除部门的操作,这步执行完毕,数据库表 dept 中的数据就已经删除了。
执行 1/0 操作,抛出异常
抛出异常之前,下面所有的代码都不会执行了,根据部门ID删除该部门下的员工,这个操作也不会执行 。
此时就出现问题了,部门删除了,部门下的员工还在,业务操作前后数据不一致。
而要想保证操作前后,数据的一致性,就需要让解散部门中涉及到的两个业务操作,要么全部成功,要么全部失败 。 那我们如何,让这两个操作要么全部成功,要么全部失败呢 ?
此时,我们就需要在delete删除业务功能中添加事务。
提问,那怎样来控制事务(事务控制的模式)?
思考:开发中所有的业务操作,一旦我们要进行控制事务,是不是都是这样的套路?
答案:是的。
所以在Spring框架当中就已经把事务控制的代码都已经封装好了,并不需要我们手动实现。
- @Transactional是Spring当中提供的一个事务管理的注解;
- 它的作用就是在当前方法执行开始之前来开启事务,方法执行完毕后来提交事务,如果该方法在执行的过程中出现了异常,Spring会自动地进行事务的回滚操作。
- @Transactional注解一般会在业务层当中来控制事务,因为在业务层当中一个业务功能可能包含多个数据访问的操作,在业务层来控制事务,我们就可以将多个数据访问操作控制在一个事务范围内。
- @Transactional注解不仅可以作用在方法上,也可以作用在类和接口上。
- 如果作用在方法上,代表当前方法交给Spring进行事务管理;
- 如果作用在类上,代表当前类当中所有的方法都交由Spring进行事务管理;
- 如果是直接作用在接口上,就代表这个接口下所有的实现类当中所有的方法都交给Spring进行事务管理。
- 虽然@Transactional注解可以作用在方法上、类或接口上,但是一般我们会选择加在Servcice业务层的增删改这一类的方法上,更加准确的说是加在Service业务层当中执行多次数据访问操作这一类的增删改方法上,因为查询操作它并不会影响数据的变更,所以无需控制事务,而如果在这个业务方法当中只是执行了一步简单的增删改操作,我们也不需要控制事务,因为MySQL数据库的事务是自动提交的,我们DML语句执行完毕之后,如果执行成功,事务就已经自动提交了;如果执行失败,数据库当中的数据也不会发生变更,所以,也不需要控制事务。我们只需要在类似于像delete方法这样的,执行了多次数据访问操作的业务方法上加上@Transactional注解来控制事务就可以了。
接下来,我们就可以在业务方法delete上加上 @Transactional 来控制事务 。
package com.gch.service.impl;
/**
部门业务实现类
*/
@Slf4j
@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;
@Autowired
private EmpMapper empMapper;
/**
* 根据部门id来删除对应的部门 - 事务
* @param id
*/
@Transactional // 表示当前方法已经交给Spring进行事务管理
@Override
public void delete(Integer id) {
// 1.根据部门id来删除部门
deptMapper.deleteById(id);
// 模拟:异常发生
int i = 1 / 0;
// 2.根据部门id来删除部门下的所有员工信息
empMapper.deleteByDeptId(id);
}
}
说明:可以在application.yml配置文件中开启事务管理日志,这样就可以在控制台看到和事务相 关的日志信息了 Spring事务管理日志相关类 - JdbcTransactionManager
#开启Spring进行事务管理的日志开关{spring事务管理日志}
logging:
level:
org.springframework.jdbc.support.JdbcTransactionManager: debug
在业务功能上添加@Transactional注解进行事务管理后,我们重启SpringBoot服务,添加Spring事务管理后,由于服务端程序引发了异常,所以事务进行回滚。
一旦出现异常,Spring会自动地进行事务地回滚。
前面我们通过Spring事务管理注解@Transactional已经控制了业务层方法的事务。
接下来我们要来详细的介绍一下@Transactional事务管理注解的使用细节。我们这里主要介绍@Transactional注解当中的两个常见的属性:
异常回滚的属性:rollbackFor
事务传播行为:propagation
我们先来学习下rollbackFor属性。
我们在之前编写的业务方法上添加了@Transactional注解,来实现事务管理。
@Transactional
public void delete(Integer id){
//根据部门id删除部门信息
deptMapper.deleteById(id);
//模拟:异常发生
int i = 1/0;
//删除部门下的所有员工信息
empMapper.deleteByDeptId(id);
}
以上业务功能delete()方法在运行时,会引发除0的算数运算异常(运行时异常),出现异常之后,由于我们在方法上加了@Transactional注解进行事务管理,所以发生异常会执行rollback回滚操作,从而保证事务操作前后数据是一致的。
下面我们在做一个测试,我们修改业务功能代码,在模拟异常的位置上直接抛出Exception异常(编译时异常)
/**
* 根据部门id来删除对应的部门 - 事务
* @param id
*/
@Transactional // 表示当前方法已经交给Spring进行事务管理
@Override
public void delete(Integer id) throws Exception {
// 1.根据部门id来删除部门
deptMapper.deleteById(id);
// 模拟:异常发生
// int i = 1 / 0;
if(true){
throw new Exception("出现异常了...");
}
// 2.根据部门id来删除部门下的所有员工信息
empMapper.deleteByDeptId(id);
}
说明:在Service中向上抛出一个Exception编译时异常之后,由于是Controller调用Service,所以在Controller中要有异常处理代码,此时我们选择在Controller中继续把异常向上抛。
重新启动服务后测试,抛出异常之后事务会不会回滚?
发生了Exception异常,但事务依然提交了,抛出异常之后没有进行事务回滚。
通过以上测试可以得出一个结论:默认情况下,只有出现RuntimeException(运行时异常)才会回滚事务。
假如我们想让所有的异常都回滚,需要来配置@Transactional注解当中的rollbackFor属性,通过rollbackFor这个属性可以指定出现何种异常类型回滚事务。
/**
* 根据部门id来删除对应的部门 - 事务
* @param id
*/
@Transactional(rollbackFor = Exception.class) // 表示当前方法已经交给Spring进行事务管理
@Override
public void delete(Integer id) throws Exception {
// 1.根据部门id来删除部门
deptMapper.deleteById(id);
// 模拟:异常发生
// int i = 1 / 0;
if(true){
throw new Exception("出现异常了...");
}
// 2.根据部门id来删除部门下的所有员工信息
empMapper.deleteByDeptId(id);
}
接下来我们重新启动服务,测试删除部门的操作:
在Spring的事务管理中,默认只有运行时异常 RuntimeException才会回滚。
如果还需要指定出现何种异常才会进行事务的回滚,可以通过rollbackFor属性来指定。
@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
接下来我们就通过一个案例来演示下事务传播行为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
package com.gch.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
日志表
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class DeptLog {
/** 主键ID */
private Integer id;
/** 操作时间 */
private LocalDateTime createTime;
/** 日志的描述信息 */
private String description;
}
DeptLogService
package com.gch.service;
import com.gch.pojo.DeptLog;
/**
日志表业务规则/接口
*/
public interface DeptLogService {
/**
* 往日志表当中插入数据
* @param deptLog
*/
void insert(DeptLog deptLog);
}
DeptLogServiceImpl
package com.gch.service.impl;
import com.gch.mapper.DeptLogMapper;
import com.gch.pojo.DeptLog;
import com.gch.service.DeptLogService;
/**
日志表业务实现类
*/
@Service
public class DeptLogServiceImpl implements DeptLogService {
@Autowired
private DeptLogMapper deptLogMapper;
/**
* 往日志表当中插入数据
* @param deptLog
*/
@Transactional(rollbackFor = Exception.class) // 事务传播行为:有事务就加入,没有事务就新建事务
@Override
public void insert(DeptLog deptLog) {
// 调用mapper往日志表当中来插入数据
deptLogMapper.insert(deptLog);
}
}
DeptLogMapper
package com.gch.mapper;
import com.gch.pojo.DeptLog;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
/**
日志表
*/
@Mapper
public interface DeptLogMapper {
/**
* 插入数据
* @param deptLog
*/
@Insert("insert into dept_log(create_time, description) values (#{createTime},#{description})")
void insert(DeptLog deptLog);
}
业务实现类:DeptServiceImpl
package com.gch.service.impl;
import com.gch.mapper.DeptMapper;
import com.gch.mapper.EmpMapper;
import com.gch.pojo.Dept;
import com.gch.pojo.DeptLog;
import com.gch.service.DeptLogService;
import com.gch.service.DeptService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
/**
部门业务实现类
*/
@Slf4j
@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;
@Autowired
private EmpMapper empMapper;
@Autowired
private DeptLogService deptLogService;
/**
* 根据部门id来删除对应的部门以及部门下所有的员工 - 事务
* @param id
*/
@Transactional(rollbackFor = Exception.class) // 表示当前方法已经交给Spring进行事务管理
@Override
public void delete(Integer id) throws Exception {
try{
// 1.根据部门id来删除部门
deptMapper.deleteById(id);
// 模拟:异常发生
if(true){
throw new Exception("出现异常了...");
}
// 2.根据部门id来删除部门下的所有员工信息
empMapper.deleteByDeptId(id);
}finally { // 不论是否有异常,最终都要执行的代码:记录日志
// 记录日志
DeptLog deptLog = new DeptLog();
deptLog.setCreateTime(LocalDateTime.now());
deptLog.setDescription("执行了解散部门的操作,此次解散的是" + deptLog.getId() + "号部门!");
// 调用其它业务类的方法,往日志表当中来插入日志
deptLogService.insert(deptLog);
}
}
}
测试:
重新启动SpringBoot服务,测试删除3号部门后会发生什么?
执行了删除3号部门操作
执行了插入部门日志操作
程序发生Exception异常
执行事务回滚(删除、插入操作因为在一个事务范围内,两个操作共用同一个事务,两个操作都会被回滚)
原因分析:
接下来我们就需要来分析一下具体是什么原因导致的日志没有成功的记录。
在执行delete操作时开启了一个事务,delete调insert
当执行insert操作时,insert设置的事务传播行是默认值REQUIRED,表示有事务就加入,没有则新建事务
此时:delete和insert操作使用了同一个事务,同一个事务中的多个操作,要么同时成功,要么同时失败,所以当异常发生时进行事务回滚,就会回滚delete和insert操作
解决方案:
在DeptLogServiceImpl类中insert方法上,添加@Transactional(propagation = Propagation.REQUIRES_NEW)
- Propagation.REQUIRES_NEW :不论是否有事务,都创建新事务 ,运行在一个独立的事务中。
package com.gch.service.impl;
import com.gch.mapper.DeptLogMapper;
import com.gch.pojo.DeptLog;
import com.gch.service.DeptLogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
/**
日志表业务实现类
*/
@Service
public class DeptLogServiceImpl implements DeptLogService {
@Autowired
private DeptLogMapper deptLogMapper;
/**
* 往日志表当中插入数据
* @param deptLog
*/
@Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRES_NEW) // 事务传播行为:有事务就加入,没有事务就新建事务
@Override
public void insert(DeptLog deptLog) {
// 调用mapper往日志表当中来插入数据
deptLogMapper.insert(deptLog);
}
}
那此时,DeptServiceImpl中的delete方法运行时,会开启一个事务。 当调用 deptLogService.insert(deptLog) 时,也会创建一个新的事务,那此时,当insert方法运行完毕之后,事务就已经提交了。 即使外部的事务出现异常,内部已经提交的事务,也不会回滚了,因为是两个独立的事务。
到此事务传播行为已演示完成,事务的传播行为我们只需要掌握两个:REQUIRED、REQUIRES_NEW。
REQUIRED :大部分情况下都是用该传播行为即可。
REQUIRES_NEW :当我们不希望两个事务之间相互影响时,可以使用该传播行为。A事务运行的时候不要影响B事务,即使A事务出错了,也不要影响B事务的结果,这个时候就可以使用REQUIRES_NEW。最典型的场景就是日志记录。比如:下订单前需要记录日志,不论订单保存成功与否,都需要保证日志记录能够记录成功。