事务回滚与传播
1. 保证增删改事务安全
什么是事务: 单次执行的增删改等一系列操作,这些操作要么全部成功执行,要么全部失败回滚
2. 如何保证事务?
最简单的方法:使用@Transactional(rollbackFor = Exception.class)注解
注解可以用在方法和类上。
方法要求:只能用在被重写的方法上面
类要求:写在@Service的类上面
举个栗子:单表service方法里的插入
@Service
public class WorkOperatorServiceImpl extends ServiceImpl<WorkOperatorMapper, WorkOperator> implements IWorkOperatorService {
@Override
@Transactional(rollbackFor = Exception.class)
public boolean createWorkOperator(WorkOperatorDTO saveDTO) {
// DTO -> entity
// 需要额外查询 片区、片区id、作业单位名称、年龄
WorkOperator saveOne = transDTO2Entity(saveDTO);
saveOne.setId(null);
// 保存
return workOperatorDAO.insertWorkOperator(saveOne);
}
}
再举个栗子:多表数据操作
场景如下: 某一作业任务下有多个作业点位信息,一对多
有两张表 ,点位表和任务表
task : id, name, points(JSON,存储点位信息 id)
points : id, address, coordinate(JSON,存储点位经纬度坐标)
重点: 删除任务时,需要同步删除任务下的点位信息
@Service
public class GuaranteeTaskServiceImpl extends ServiceImpl<GuaranteeTaskMapper, GuaranteeTask> implements GuaranteeTaskService {
/** 点位表 */
@Resource
GuaranteeTaskPointsServiceImpl pointsService;
/** 任务表 */
@Resource
GuaranteeTaskAssignService assignService;
/**
* 根据任务 id 删除 点位和自己
*
* @param id 任务 id
* @return
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean deleteById(Long id) {
GuaranteeTask oneTask = lambdaQuery().eq(GuaranteeTask::getId, id).one();
// 删除点位,点位表操作
pointsService.removeBatchByIds(oneTask.getPoints());
// 删除分配任务里的 task-id = id 的数据
return assignService.remove(new QueryWrapper<GuaranteeTaskAssign>().eq("task_id", id));
}
}
这样一来,添加注解后的方法里,要么全部执行成功,要么全部回滚
3. 不想全部回滚,只想回滚一部分怎么办?
场景如下: 事务操作时,分为两步,第一步进行业务代码逻辑,第二部分进行日志的记录输出,业务代码作为主体功能,优先实现,日志不输出也无所谓。
出现的问题:按照上面的写法,添加单个注解后,业务代码执行成功,日志输出失败,但是主体功能也会回滚,导致此次业务办理失败。
预期目标:日志失败,但是业务成功实现
解决方案:
使用事务传播进行解决
3.1 事务实现的原理 - 切面实现
事务可以手动提交,也可以自动提交,所以,在事务操作前后,添加上事务手动提交的代码,改变当前事务的提交模式,代码执行无误时,commit事务,有误时,进行回滚(也就是不提交)
不自动提交事务相当于: 去饭馆吃饭没给钱,先吃饭
饭吃完后,付款,付款这一步就是 commit 操作
饭吃到一半,发现饭里有东西,开始找老板处理。找老板处理,不给钱这一步就是不提交,回滚操作,rollback
代码上手动实现事务操作如下:
class Transactional {
// 建立数据库连接,进行操作
s1 = new SqlSession();
sqlSession.open();
// 设置事务为手动提交
s1.setAutoCommit(false);
try{
// 事务处理 Method()
Method_1();
Method_2();
Method_3();
// 处理完毕后,开始提交
commit();
} catch(Exception e){
// 发现异常就回滚
rollback();
} finally {
// 关闭和数据库的连接
s1.close();
}
}
如果每个事务操作都要这么写,那可就太蠢啦,所以直接使用了注解进行切面的环绕通知,完成了这一段重复性很强的代码。
切面 : 就是在某个方法运行时,对该方法的入参和结果添加一些新的处理逻辑,以达到增强原来方法的作用,后续讲到
使用注解后,上面的代码就变成了下面的样子,便于更加专注业务实现
class Transactional {
@Transactional(rollbackFor = Exception.class)
public Method(){
Method_1();
Method_2();
Method_3();
}
}
3.2 事务传播实现
讲完了原理,开始解决上面提到的日志事务问题
回滚操作,是在一个数据库连接中处理的,如果给主要的业务方法,再开一个新的数据库连接,对这个新连接进行事务操作,不影响原先的连接,不就OK了嘛
举个不太恰当的栗子: 你买了两份饭,A 和 B,A份有问题,B没有问题。经过和老板商量,A不用付钱,赔你一份新的(A 回滚),对于 B 还是得付钱(B 提交)
代码中如何实现呢?
- 日志方法
class A {
// 打印日志,进入总方法的sqlSession
@Transactional(rollbackFor = Exception.class)
public void logInfo();
}
- 业务方法
class B {
// 业务操作,新开一个连接,回滚该连接的数据
@Transactional(rollbackFor = Exception.class, propagation = REQUIRES_NEW)
public void Method();
}
- 对于上面所述的日志问题处理方法,外部和内部的属性值看情况而定
这样一来,业务方法Method(); 不会因 logInfo();失败导致回滚了。
class dealTransactional {
@Transactional(rollbackFor = Exception.class)
public void finallMethod() {
logInfo();
Method();
}
}
- propagation 属性值的说明
新开连接事务操作的图解如下: