参考资料
- 导致 Spring 事务失效常见的几种情况
- SpringBoot2异常处理回滚事务详解(自动回滚/手动回滚/部分回滚)
- Spring,为内部方法新起一个事务,此处应有坑。
- PlatformTransactionManager
- Spring 事务管理及失效总结
- 我认真总结并分析了 Spring 事务失效的十种常见场景
- SpringBoot AOP配置全局事务
目录
一. 需求
最近在项目中遇到了一个需求,需要读取csv的数据到数据库的临时表,
然后调用存储过程将临时表中的数据插入正式的数据库中。
⏹在将csv数据插入临时表之前需要将该表中的数据清空,如果在插入临时表时出现了问题,同样需要将表中的数据清空。
⏹需要对前台传入的数据进行业务校验,如果校验失败,需要抛出自定义异常,同时临时表中的数据不能回滚,需要被清空。
⏹在调用存储过程时,如果发生异常,则事务回滚。
需要注意的是,只回滚存储过程所涉及的表,临时表中被删除的数据并不参与回滚,直接删除。
二. 前期准备
2.1 Mapper
import java.util.Map;
public interface TestMapper02 {
void deleteAllTempWork();
void insertData(Map<String, Object> dataMap);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.TestMapper02">
<delete id="deleteAllTempWork">
DELETE FROM
temp_work
</delete>
<insert id="insertData" parameterType="map">
INSERT
INTO day16.`user`(id, username, birthday, email)
VALUES (#{id}, #{username}, #{birthday}, #{email})
</insert>
</mapper>
2.2 自定义异常
public class ValidationException extends RuntimeException {
private static final long serialVersionUID = 1L;
}
2.3 前台
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>事务测试</title>
</head>
<body>
<button id="btn1">点击发送请求</button>
</body>
<script th:src="@{/js/public/jquery-3.6.0.min.js}"></script>
<script th:inline="javascript">
$("#btn1").click(function() {
const data = {
from: 13,
to: 14
}
$.ajax({
url: "/test02/transactional",
type: 'POST',
data: JSON.stringify(data),
contentType: 'application/json;charset=utf-8',
success: function (data, status, xhr) {
console.log(data);
}
});
});
</script>
</html>
2.4 Controller层
import java.util.Map;
import javax.annotation.Resource;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
@Controller
@RequestMapping("/test02")
public class Test02Controller {
@Resource
private Test02Service service;
@GetMapping("/init")
public ModelAndView init() {
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("test02");
return modelAndView;
}
@PostMapping("/transactional")
public ResponseEntity<Void> transactional(@RequestBody Map<String, Integer> data) throws Exception {
// 测试事务
service.transactional1(data);
// service.transactional2(data);
// service.transactional3(data);
// 无响应给前台
return ResponseEntity.noContent().build();
}
}
三. 错误的使用方式
- transactional1主方法调用同一个类中的insertData子方法,并且给子方法添加了
@Transactional
注解。 - 出错的原因:
- 在应用系统调用声明
@Transactional
的目标方法时,Spring Framework 默认使用AOP
代理,在代码运行时生成一个代理对象,再由这个代理对象来统一管理。 - Spring 事务是使用
AOP
环绕通知和异常通知,就是对方法进行拦截,在方法执行前开启事务,在捕获到异常时进行事务回滚,在方法执行完成后提交事务。 - 在 Spring 的 AOP 代理下,只有目标方法由外部调用,目标方法才由 Spring 生成的代理对象来管理。若同一类中的其他没有
@Transactional
注解的方法内部调用有@Transactional
注解的方法,有@Transactional
注解的方法的事务被忽略,不会发生回滚。 - 👉👉👉spring是通过aop的方式,对需要spring管理事务的bean生成了代理对象,然后通过代理对象拦截了目标方法的执行,在方法前后添加了事务的功能,所以必须通过代理对象调用目标方法的时候,事务才会起效。
- 在应用系统调用声明
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.example.demo.mapper.TestMapper02;
@Service
public class Test02Service {
// 准备要向数据库插入的数据
private final static Map<String, Object> dataMap = new HashMap<>() {
private static final long serialVersionUID = 1L;
{
put("id", 99);
put("username", "test99");
put("birthday", new Date());
put("email", "test99@test.com");
}
};
@Resource
private TestMapper02 mapper02;
// 存在事务的方法
public void transactional1(Map<String, Integer> data) throws Exception {
// 删除临时表中的所有数据
mapper02.deleteAllTempWork();
// 获取from和to的值
Integer fromValue = data.get("from");
Integer toValue = data.get("to");
if (fromValue > toValue) {
// 抛出异常
throw new Exception();
}
this.insertData(dataMap);
}
/*
insertData方法被transactional1方法调用
*/
@Transactional(rollbackFor = Exception.class)
public void insertData(Map<String, Object> dataMap) {
mapper02.insertData(dataMap);
// 模拟出现异常
int reslt = 1 / 0;
}
}
四. 正确的使用方式
4.1 方式1-抽取新类(声明式事务)
⏹创建一个新类,将需要被事务管理的部分放到此类中
import java.util.Map;
import javax.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class Test02SubService {
@Resource
private TestMapper02 mapper02;
@Transactional(rollbackFor = Exception.class)
public void insertData(Map<String, Object> dataMap) throws Exception {
mapper02.insertData(dataMap);
// 模拟运行时异常
int reslt = 1 / 0;
}
}
⏹我们的主Service调用子Service,子Service由Spring来管理,会生成事务对象。
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.example.demo.mapper.TestMapper02;
@Service
public class Test02Service {
// 准备要向数据库插入的数据
private final static Map<String, Object> dataMap = new HashMap<>() {
private static final long serialVersionUID = 1L;
{
put("id", 99);
put("username", "test99");
put("birthday", new Date());
put("email", "test99@test.com");
}
};
@Resource
private Test02SubService subService;
@Resource
private TestMapper02 mapper02;
// 存在事务的方法
public void transactional1(Map<String, Integer> data) throws Exception {
// 删除临时表中的所有数据
mapper02.deleteAllTempWork();
// 获取from和to的值
Integer fromValue = data.get("from");
Integer toValue = data.get("to");
if (fromValue > toValue) {
// 抛出异常
throw new Exception();
}
// 如果调用时发生异常,只会回滚insertData方法中的内容
subService.insertData(dataMap);
}
}
4.2 方式2-编程式事务,事务管理器
package com.example.demo.service;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
@Service
public class Test02Service {
private final static Map<String, Object> dataMap = new HashMap<>() {
private static final long serialVersionUID = 1L;
{
put("id", 99);
put("username", "test99");
put("birthday", new Date());
put("email", "test99@test.com");
}
};
@Resource
private TestMapper02 mapper02;
// 事务管理器
@Autowired
private DataSourceTransactionManager dataSourceTransactionManager;
// 事务定义对象
@Autowired
private TransactionDefinition transactionDefinition;
public void transactional2(Map<String, Integer> data) throws Exception {
// 删除临时表,此部分不需要被事务管理
mapper02.deleteAllTempWork();
// 手动开启事务
TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);
try {
mapper02.insertData(dataMap);
// 模拟异常
int a = 1 / 0;
} catch (Exception e) {
// 手动回滚事务
dataSourceTransactionManager.rollback(transactionStatus);
// 抛出异常,防止程序继续执行
throw new Exception();
}
// 若没有问题则,则提交事务
dataSourceTransactionManager.commit(transactionStatus);
}
}
4.3 方式3-编程式事务,设置回滚点
- 通过
TransactionAspectSupport
设置回滚点 - rollbackFor = Exception.class:指定该异常需要回滚
- noRollbackFor = ValidationException.class:指定该异常不需要回滚
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
@Service
public class Test02Service {
private final static Map<String, Object> dataMap = new HashMap<>() {
private static final long serialVersionUID = 1L;
{
put("id", 99);
put("username", "test99");
put("birthday", new Date());
put("email", "test99@test.com");
}
};
@Resource
private TestMapper02 mapper02;
// 指定抛出Exception异常时回滚;指定抛出ValidationException异常时不回滚
@Transactional(rollbackFor = Exception.class, noRollbackFor = ValidationException.class)
public void transactional3(Map<String, Integer> data) throws Exception {
// 如果出现了任何异常就进行捕获,然后就抛出自定义的ValidationException异常
// 我们在声明式事务中指定了ValidationException异常不进行回滚,
// 因此就算此处的try catch块中的异常被抛出,此处的事务也不会进行回滚
try {
mapper02.deleteAllTempWork();
} catch (Exception e) {
throw new ValidationException();
}
// 设置事务的回滚点
Object savePoint = TransactionAspectSupport.currentTransactionStatus().createSavepoint();
try {
mapper02.insertData(dataMap);
// 模拟运行时异常
int a = 1 / 0;
} catch (Exception e) {
// 当发生异常的时候,将事务回滚到预设的回滚点处
TransactionAspectSupport.currentTransactionStatus().rollbackToSavepoint(savePoint);
}
}
}
五. 事务失效的几种情况
@EnableTransactionManagement
注解未启用- 方法
不是public类型
的- @Transaction 可以用在类上、接口上、public方法上,如果将@Trasaction用在了非public方法上,事务将无效。
三. 错误的使用方式
所示的自调用情况- spring是通过aop的方式,对需要spring管理事务的bean生成了代理对象,然后通过代理对象拦截了目标方法的执行,在方法前后添加了事务的功能,所以必须通过代理对象调用目标方法的时候,事务才会起效。
- 抛出的异常被捕获
- 当业务方法抛出异常,spring感知到异常的时候,才会做事务回滚的操作,若方法内部将异常给吞了,那么事务无法感知到异常了,事务就不会回滚了。
- 数据库
非InnoDB引擎
。- 只有InnoDB引擎才支持事务,而MyISAM引擎是不支持事务的