SpringBoot 事务回滚注意事项

参考资料

  1. 导致 Spring 事务失效常见的几种情况
  2. SpringBoot2异常处理回滚事务详解(自动回滚/手动回滚/部分回滚)
  3. Spring,为内部方法新起一个事务,此处应有坑。
  4. PlatformTransactionManager
  5. Spring 事务管理及失效总结
  6. 我认真总结并分析了 Spring 事务失效的十种常见场景
  7. 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);
		}
	}
}

五. 事务失效的几种情况

  1. @EnableTransactionManagement注解未启用
  2. 方法不是public类型
    • @Transaction 可以用在类上、接口上、public方法上,如果将@Trasaction用在了非public方法上,事务将无效。
  3. 三. 错误的使用方式所示的自调用情况
    • spring是通过aop的方式,对需要spring管理事务的bean生成了代理对象,然后通过代理对象拦截了目标方法的执行,在方法前后添加了事务的功能,所以必须通过代理对象调用目标方法的时候,事务才会起效。
  4. 抛出的异常被捕获
    • 当业务方法抛出异常,spring感知到异常的时候,才会做事务回滚的操作,若方法内部将异常给吞了,那么事务无法感知到异常了,事务就不会回滚了。
  5. 数据库非InnoDB引擎
    • 只有InnoDB引擎才支持事务,而MyISAM引擎是不支持事务的
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值