Spring中事务失效的场景有很多,本文主要分析四种常见的事务失效场景及解决方法
1. 准备工作
本次演示使用的是MySQL数据库
1.1 新建表并往表中插入测试数据
新建一张名为 account
的表,并往表中插入两条测试数据
-- ----------------------------
-- Table structure for account
-- ----------------------------
DROP TABLE IF EXISTS `account`;
CREATE TABLE `account` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`money` decimal(10, 0) NULL DEFAULT 0,
INDEX `account_id_index`(`id` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of account
-- ----------------------------
INSERT INTO `account` VALUES (1, '高启强', 10000);
INSERT INTO `account` VALUES (2, '陈书婷', 10000);
最开始的数据如下
1.2 编写简单的service层代码
import com.itheima.dao.AccountMapper;
import com.itheima.pojo.Account;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class AccountService {
@Autowired
private AccountMapper accountDao;
/**
* 转账
*
* @param from 减钱的用户id
* @param to 加钱的用户id
* @param money 金额
*/
@Transactional
public void update(Integer from, Integer to, Double money) {
// 转账的用户不能为空
Account fromAccount = accountDao.selectById(from);
// 判断用户的钱是否够转账
if (fromAccount.getMoney() - money >= 0) {
fromAccount.setMoney(fromAccount.getMoney() - money);
accountDao.updateById(fromAccount);
// 被转账的用户
Account toAccount = accountDao.selectById(to);
toAccount.setMoney(toAccount.getMoney() + money);
accountDao.updateById(toAccount);
}
}
}
1.3 编写简单的测试类
import com.itheima.Application;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@SpringBootTest(classes = Application.class)
@RunWith(SpringRunner.class)
public class AccountServiceTest {
@Autowired
private AccountService accountService;
@Test
public void update() {
accountService.update(1, 2, 500d);
}
}
2. 失效场景一(重点)
2.1 失效原因:手动捕获了异常,没有将异常抛出
在service层的代码中添加 int temp = 1 / 0;
,手动制造异常,并将该异常捕获
import com.itheima.dao.AccountMapper;
import com.itheima.pojo.Account;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class AccountService {
@Autowired
private AccountMapper accountDao;
/**
* 转账
*
* @param from 减钱的用户id
* @param to 加钱的用户id
* @param money 金额
*/
@Transactional
public void update(Integer from, Integer to, Double money) {
try {
// 转账的用户不能为空
Account fromAccount = accountDao.selectById(from);
// 判断用户的钱是否够转账
if (fromAccount.getMoney() - money >= 0) {
fromAccount.setMoney(fromAccount.getMoney() - money);
accountDao.updateById(fromAccount);
int temp = 1 / 0;
// 被转账的用户
Account toAccount = accountDao.selectById(to);
toAccount.setMoney(toAccount.getMoney() + money);
accountDao.updateById(toAccount);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
不出意外,运行测试类后报错了
在数据库中查看两人的账户余额,发现高启强的账户余额减少了500,但是陈书婷的账户余额却没有增加500,出现了数据不一致的情况
2.2 解决办法:手动捕获异常后,再次手动抛出一个新的异常
我们将异常捕获后,手动抛出一个新的异常(RunTimeException)
import com.itheima.dao.AccountMapper;
import com.itheima.pojo.Account;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class AccountService {
@Autowired
private AccountMapper accountDao;
/**
* 转账
*
* @param from 减钱的用户id
* @param to 加钱的用户id
* @param money 金额
*/
@Transactional
public void update(Integer from, Integer to, Double money) {
try {
// 转账的用户不能为空
Account fromAccount = accountDao.selectById(from);
// 判断用户的钱是否够转账
if (fromAccount.getMoney() - money >= 0) {
fromAccount.setMoney(fromAccount.getMoney() - money);
accountDao.updateById(fromAccount);
int temp = 1 / 0;
// 被转账的用户
Account toAccount = accountDao.selectById(to);
toAccount.setMoney(toAccount.getMoney() + money);
accountDao.updateById(toAccount);
}
} catch (Exception e) {
throw new RuntimeException("转账失败");
}
}
}
再次运行测试类
在数据库中查看两人的账户余额
可以看到,虽然这次转账失败了,但两个用户的账户余额没有改变,说明Spring帮我们回滚了事务
3. 失效场景二(重点)
3.1 补充:检查异常的概念
- 在Java编程语言中,异常分为两种主要类型:检查异常(Checked Exceptions)和非检查异常(Unchecked Exceptions)
- 非检查异常(Unchecked Exceptions)包括运行时异常(Runtime Exceptions)和错误(Errors)
- 检查异常是那些在编译时必须被显式捕获或声明的异常。这意味着如果一个方法可能会抛出一个检查异常,那么调用这个方法的地方必须提供一个异常处理机制,要么是通过
try-catch
语句捕获异常,要么是在方法签名中使用throws
关键字声明异常,让异常传播到更高级别的处理代码 - 检查异常通常用于表示那些在程序运行期间可能发生的、合理的、可以恢复的错误情况,比如输入/输出错误(如文件不存在)、数据库错误、网络问题等。这些异常是编译器检查的一部分,因此称为“检查异常”
常见的检查异常有
- I/O 异常 - 当进行文件读写、网络操作等I/O操作时可能出现的异常
java.io.IOException
:通用I/O异常的基类java.io.FileNotFoundException
:尝试打开不存在的文件时抛出java.io.EOFException
:当预期中应该有更多数据时却遇到文件结束符抛出java.io.UnsupportedEncodingException
:不支持指定的字符编码时抛出
- 类加载和反射异常 - 当加载类或使用反射时可能出现的异常
java.lang.ClassNotFoundException
:尝试加载不存在的类时抛出java.lang.NoSuchMethodException
:请求的方法不存在时抛出java.lang.IllegalAccessException
:尝试反射地执行一个没有访问权限的方法时抛出
- 数据库异常 - 当与数据库交互时可能出现的异常
java.sql.SQLException
:SQL执行错误或数据库访问错误时抛出
- 远程方法调用(RMI)异常 - 当使用Java的远程方法调用技术时可能出现的异常
java.rmi.RemoteException
:远程方法调用过程中出现的异常。
- 网络异常 - 当进行网络相关的操作时可能出现的异常
java.net.SocketException
:socket操作错误时抛出
- XML解析异常 - 当解析XML文档时可能出现的异常
javax.xml.parsers.ParserConfigurationException
:解析器配置错误时抛出org.xml.sax.SAXException
:SAX解析过程中出现的异常
- 数据转换和格式化异常 - 当进行数据类型转换或格式化时可能出现的异常
java.text.ParseException
:解析字符串时格式不正确时抛出
- 配置异常 - 当读取或解析配置文件时可能出现的异常
java.util.PropertiesFormatException
:解析属性文件格式错误时抛出
3.2 失效原因:抛出的异常为检查异常
我们在代码中读取一个不存在的文件,并将原来的 try-catch
语句去掉,直接在方法中抛出 FileNotFoundException
异常
import com.itheima.dao.AccountMapper;
import com.itheima.pojo.Account;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
@Service
public class AccountService {
@Autowired
private AccountMapper accountDao;
/**
* 转账
*
* @param from 减钱的用户id
* @param to 加钱的用户id
* @param money 金额
*/
@Transactional
public void update(Integer from, Integer to, Double money) throws FileNotFoundException {
// 转账的用户不能为空
Account fromAccount = accountDao.selectById(from);
// 判断用户的钱是否够转账
if (fromAccount.getMoney() - money >= 0) {
fromAccount.setMoney(fromAccount.getMoney() - money);
accountDao.updateById(fromAccount);
FileInputStream fileInputStream = new FileInputStream("Tom.txt");
// 被转账的用户
Account toAccount = accountDao.selectById(to);
toAccount.setMoney(toAccount.getMoney() + money);
accountDao.updateById(toAccount);
}
}
}
不出意外,运行测试类后程序报错了
在数据库中查看两人的账户余额,发现高启强的账户余额减少了500,但是陈书婷的账户余额却没有增加,出现了数据不一致的情况
3.3 解决方法:在@Transactional注解中指定触发事务回滚的异常类型
默认情况下,只有抛出的异常类型为非检查异常,Spring才会帮我们回滚事务
我们可以在 @Transactional
注解中指定触发事务回滚的异常类型为 Exception
,只要抛出了异常,就回滚事务,具体格式为 @Transactional(rollbackFor = Exception.class)
import com.itheima.dao.AccountMapper;
import com.itheima.pojo.Account;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
@Service
public class AccountService {
@Autowired
private AccountMapper accountDao;
/**
* 转账
*
* @param from 减钱的用户id
* @param to 加钱的用户id
* @param money 金额
*/
@Transactional(rollbackFor = Exception.class)
public void update(Integer from, Integer to, Double money) throws FileNotFoundException {
// 转账的用户不能为空
Account fromAccount = accountDao.selectById(from);
// 判断用户的钱是否够转账
if (fromAccount.getMoney() - money >= 0) {
fromAccount.setMoney(fromAccount.getMoney() - money);
accountDao.updateById(fromAccount);
FileInputStream fileInputStream = new FileInputStream("Tom.txt");
// 被转账的用户
Account toAccount = accountDao.selectById(to);
toAccount.setMoney(toAccount.getMoney() + money);
accountDao.updateById(toAccount);
}
}
}
再次运行测试类,可以发现,虽然抛出了异常,但数据库中的数据并没有发生变化,说明Spring帮我们回滚了事务
4. 失效场景三
4.1 失效原因:使用@Transactional注解的方法的权限不是public
将update方法前面的public关键字去掉,重新运行测试类
@Service
public class AccountService {
@Autowired
private AccountMapper accountDao;
/**
* 转账
*
* @param from 减钱的用户id
* @param to 加钱的用户id
* @param money 金额
*/
@Transactional(rollbackFor = Exception.class)
void update(Integer from, Integer to, Double money) throws FileNotFoundException {
// 转账的用户不能为空
Account fromAccount = accountDao.selectById(from);
// 判断用户的钱是否够转账
if (fromAccount.getMoney() - money >= 0) {
fromAccount.setMoney(fromAccount.getMoney() - money);
accountDao.updateById(fromAccount);
FileInputStream fileInputStream = new FileInputStream("Tom.txt");
// 被转账的用户
Account toAccount = accountDao.selectById(to);
toAccount.setMoney(toAccount.getMoney() + money);
accountDao.updateById(toAccount);
}
}
}
运行测试类后,程序报错,但数据库中两个用户的账号余额却出现了不一致的情况(高启强的账户余额减少了500,但是陈书婷的账户余额却没有增加500)
如果你使用@Transactional注解的方法的权限不是public,IDEA会给出明确的警告(已经上升到错误级别)
平时多留意IDEA给出的警告,能大大减少程序出错的概率
4.2 解决方法:将使用@Transactional注解的方法的权限设置为public
5. 失效场景四(重点)
5.1 失效原因:一个非事务方法调用了一个标记为 @Transactional 的事务方法
我们在AccountService
中添加一个名为callUpdate
的方法,在该方法中调用update
方法
import com.itheima.dao.AccountMapper;
import com.itheima.pojo.Account;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class AccountService {
@Autowired
private AccountMapper accountDao;
/**
* 调用转账方法
*
* @param from 减钱的用户id
* @param to 加钱的用户id
* @param money 金额
*/
public void callUpdate(Integer from, Integer to, Double money) {
update(from, to, money);
}
/**
* 转账
*
* @param from 减钱的用户id
* @param to 加钱的用户id
* @param money 金额
*/
@Transactional(rollbackFor = Exception.class)
public void update(Integer from, Integer to, Double money) {
try {
// 转账的用户不能为空
Account fromAccount = accountDao.selectById(from);
// 判断用户的钱是否够转账
if (fromAccount.getMoney() - money >= 0) {
fromAccount.setMoney(fromAccount.getMoney() - money);
accountDao.updateById(fromAccount);
int temp = 1 / 0;
// 被转账的用户
Account toAccount = accountDao.selectById(to);
toAccount.setMoney(toAccount.getMoney() + money);
accountDao.updateById(toAccount);
}
} catch (Exception e) {
throw new RuntimeException("转账失败");
}
}
}
转账前双方的余额
运行测试类的testCallUpdate方法
,抛出异常后,高启强的账户余额减少了500,但是陈书婷的账户余额却没有增加500
5.2 失效原因详解
5.2.1 补充:自调用的概念
在详细分析事务失效之前,我们先了解什么是自调用
自调用:在同一个类中,非事务方法直接调用事务方法称为自调用
IEDA对自调用给出了明确的警告(多看IDEA的提示,能大大减少程序出错的概率)
5.2.2 Spring事务管理的原理
Spring 的事务管理的底层基于 Spring AOP(面向切面编程)和代理模式
当在 Spring 应用程序中定义一个 bean 并使用 @Transactional
注解这个bean的某个方法时,Spring 容器会创建一个代理对象(基于 JDK 动态代理或 CGLIB动态代理)来包装原始的 bean
这个代理对象会拦截所有被 @Transactional
注解标记的方法的调用,并在方法前后加上与事务相关的逻辑
以上述的update
方法为例,当update
方法加上@Transactional
注解后,代理对象执行的方法类似于以下代码
try {
// 开启事务
update();
// 提交事务
} catch (Exception exception){
// 回滚事务
}
- 实际上,callUpdate()是一个简写,完整写法是this.callUpdate(),也就是说,执行的是当前对象的callUpdate()方法
- 如果当前对象不是代理对象,update方法就是一个普通的方法,callUpdate()方法调用update方法时并不会被代理对象拦截,Spring就不会对此次方法调用进行事务管理
5.3 解决方法
5.3.1 使用AopContext获取到当前代理类
第一步:在启动类加上以下代码
@EnableAspectJAutoProxy(exposeProxy = true)
第二步:在非事务方法中获取当前代理类,再通过代理类调用需要进行事务管理的方法
/**
* 调用转账方法
*
* @param from 减钱的用户id
* @param to 加钱的用户id
* @param money 金额
*/
public void callUpdate(Integer from, Integer to, Double money) {
AccountService accountService = (AccountService) AopContext.currentProxy();
accountService.update(from, to, money);
}
第三步:测试
转账前双方的余额
调用callUpdate()
方法转账后双方的余额
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
程序依旧抛出了异常,但Spring进行了事务管理,数据库的数据保持了一致性
5.3.2 通过ApplicationContext获取容器中的代理对象(推荐使用)
第一步:在AccountService
类中注入ApplicationContext
@Autowired
private ApplicationContext applicationContext;
第二步:从容器中获取当前类的代理对象,再通过代理对象调用需要进行事务管理的方法
/**
* 调用转账方法
*
* @param from 减钱的用户id
* @param to 加钱的用户id
* @param money 金额
*/
public void callUpdate(Integer from, Integer to, Double money) {
AccountService accountService = applicationContext.getBean(AccountService.class);
accountService.update(from, to, money);
}
第三步:测试
转账前双方的余额
调用callUpdate()
方法转账后双方的余额
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
程序依旧抛出了异常,但Spring进行了事务管理,数据库的数据保持了一致性