前言

作为Java开发工程师,相信大家对Spring种事务的使用并不陌生。但是你可能只是停留在基础的使用层面上,在遇到一些比较特殊的场景,事务可能没有生效,直接在生产上暴露了,这可能就会导致比较严重的生产事故。今天,我们就简单来说下Spring事务的原理,然后总结一下spring事务失败的场景,并提出对应的解决方案。

Spring事务原理

大家还记得在JDBC中是如何操作事务的吗?伪代码可能如下:

//Get database connection
Connection connection = DriverManager.getConnection();
//Set autoCommit is false
connection.setAutoCommit(false);
//use sql to operate database
.........
//Commit or rollback
connection.commit()/connection.rollback connection.close();

需要在各个业务代码中编写代码如commit()close()来控制事务。

但是Spring不乐意这么干了,这样对业务代码侵入性太大了,所有就用一个事务注解@Transactional来控制事务,底层实现是基于切面编程AOP实现的,而Spring中实现AOP机制采用的是动态代理,具体分为JDK动态代理和CGLIB动态代理两种模式。

  1. Springbean的初始化过程中,发现方法有Transactional注解,就需要对相应的Bean进行代理,生成代理对象。
  2. 然后在方法调用的时候,会执行切面的逻辑,而这里切面的逻辑中就包含了开启事务、提交事务或者回滚事务等逻辑。

另外注意一点的是,Spring 本身不实现事务,底层还是依赖于数据库的事务。没有数据库事务的支持,Spring事务是不会生效的。

接下来我们进入正题,看看哪些场景会导致Spring事务失败。

Spring事务失效场景

1. 抛出检查异常

比如你的事务控制代码如下:

@Transactional
public void transactionTest() throws IOException{
User user = new User();
UserService.insert(user);
throw new IOException();
}

如果@Transactional 没有特别指定,Spring 只会在遇到运行时异常RuntimeException或者error时进行回滚,而IOException等检查异常不会影响回滚。

public boolean rollbackOn(Throwable ex) {
return (ex instanceof RuntimeException || ex instanceof Error);
}

解决方案:

知道原因后,解决方法也很简单。配置rollbackFor属性,例如@Transactional(rollbackFor = Exception.class)

2. 业务方法本身捕获了异常

@Transactional(rollbackFor = Exception.class)
public void transactionTest() {
try {
User user = new User();
UserService.insert(user);
int i = 1 / 0;
}catch (Exception e) {
e.printStackTrace();
}
}

这种场景下,事务失败的原因也很简单,Spring是否进行回滚是根据你是否抛出异常决定的,所以如果你自己捕获了异常,Spring 也无能为力。

看了上面的代码,你可能认为这么简单的问题你不可能犯这么愚蠢的错误,但是我想告诉你的是,我身边几乎一半的人都被这一幕困扰过。

写业务代码的时候,代码可能比较复杂,嵌套的方法很多。如果你不小心,很可能会触发此问题。举一个非常简单的例子,假设你有一个审计功能。每个方法执行后,审计结果保存在数据库中,那么代码可能会这样写。

@Service
public class TransactionService { @Transactional(rollbackFor = Exception.class)
public void transactionTest() throws IOException {
User user = new User();
UserService.insert(user);
throw new IOException(); }
} @Component
public class AuditAspect { @Autowired
private auditService auditService; @Around(value = "execution (* com.alvin.*.*(..))")
public Object around(ProceedingJoinPoint pjp) {
try {
Audit audit = new Audit();
Signature signature = pjp.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
String[] strings = methodSignature.getParameterNames();
audit.setMethod(signature.getName());
audit.setParameters(strings);
Object proceed = pjp.proceed();
audit.success(true);
return proceed;
} catch (Throwable e) {
log.error("{}", e);
audit.success(false);
} auditService.save(audit);
return null;
} }

在上面的示例中,事务将失败。原因是Spring的事务切面优先级最低,所以如果异常被切面捕获,Spring自然不能正常处理事务,因为事务管理器无法捕获异常。

解决方案:

看,虽然我们知道在处理事务时业务代码不能自己捕获异常,但是只要代码变得复杂,我们就很可能再次出错,所以我们在处理事务的时候要小心,还是不要使用声明式事务, 并使用编程式事务— transactionTemplate.execute()

3. 同一类中的方法调用

@Service
public class DefaultTransactionService implement Service { public void saveUser() throws Exception {
//do something
doInsert();
} @Transactional(rollbackFor = Exception.class)
public void doInsert() throws IOException {
User user = new User();
UserService.insert(user);
throw new IOException(); }
}

这也是一个容易出错的场景。事务失败的原因也很简单,因为Spring的事务管理功能是通过动态代理实现的,而Spring默认使用JDK动态代理,而JDK动态代理采用接口实现的方式,通过反射调用目标类。简单理解,就是saveUser()方法中调用this.doInsert(),这里的this是被真实对象,所以会直接走doInsert的业务逻辑,而不会走切面逻辑,所以事务失败。

解决方案:

方案一:解决方法可以是直接在启动类中添加@Transactional注解saveUser()

方案二@EnableAspectJAutoProxy(exposeProxy = true)在启动类中添加,会由Cglib代理实现。

4. 方法使用 final 或 static关键字

如果Spring使用了Cglib代理实现(比如你的代理类没有实现接口),而你的业务方法恰好使用了final或者static关键字,那么事务也会失败。更具体地说,它应该抛出异常,因为Cglib使用字节码增强技术生成被代理类的子类并重写被代理类的方法来实现代理。如果被代理的方法的方法使用finalstatic关键字,则子类不能重写被代理的方法。

如果Spring使用JDK动态代理实现,JDK动态代理是基于接口实现的,那么finalstatic修饰的方法也就无法被代理。

总而言之,方法连代理都没有,那么肯定无法实现事务回滚了。

解决方案:

想办法去掉final或者static关键字

5. 方法不是public

如果方法不是publicSpring事务也会失败,因为Spring的事务管理源码AbstractFallbackTransactionAttributeSource中有判断computeTransactionAttribute()。如果目标方法不是公共的,则TransactionAttribute返回null

// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}

解决方案:

是将当前方法访问级别更改为public

6. 错误使用传播机制

Spring事务的传播机制是指在多个事务方法相互调用时,确定事务应该如何传播的策略。Spring提供了七种事务传播机制:REQUIREDSUPPORTSMANDATORYREQUIRES_NEWNOT_SUPPORTEDNEVERNESTED。如果不知道这些传播策略的原理,很可能会导致交易失败。

@Service
public class TransactionService { @Autowired
private UserMapper userMapper; @Autowired
private AddressMapper addressMapper; @Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)
public void doInsert(User user,Address address) throws Exception {
//do something
userMapper.insert(user);
saveAddress(address);
} @Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveAddress(Address address) {
//do something
addressMapper.insert(address);
}
}

在上面的例子中,如果用户插入失败,不会导致saveAddress()回滚,因为这里使用的传播是REQUIRES_NEW,传播机制REQUIRES_NEW的原理是如果当前方法中没有事务,就会创建一个新的事务。如果一个事务已经存在,则当前事务将被挂起,并创建一个新事务。在当前事务完成之前,不会提交父事务。如果父事务发生异常,则不影响子事务的提交。

事务的传播机制说明如下:

  • REQUIRED 如果当前上下文中存在事务,那么加入该事务,如果不存在事务,创建一个事务,这是默认的传播属性值。
  • SUPPORTS 如果当前上下文存在事务,则支持事务加入事务,如果不存在事务,则使用非事务的方式执行。
  • MANDATORY 如果当前上下文中存在事务,否则抛出异常。
  • REQUIRES_NEW 每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行。
  • NOT_SUPPORTED 如果当前上下文中存在事务,则挂起当前事务,然后新的方法在没有事务的环境中执行。
  • NEVER 如果当前上下文中存在事务,则抛出异常,否则在无事务环境上执行代码。
  • NESTED 如果当前上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务。

解决方案:

将事务传播策略更改为默认值REQUIREDREQUIRED原理是如果当前有一个事务被添加到一个事务中,如果没有,则创建一个新的事务,父事务和被调用的事务在同一个事务中。即使被调用的异常被捕获,整个事务仍然会被回滚。

7. 没有被Spring管理

// @Service
public class OrderServiceImpl implements OrderService {
@Transactional
public void updateOrder(Order order) {
// update order
}
}

如果此时把 @Service 注解注释掉,这个类就不会被加载成一个 Bean,那这个类就不会被 Spring 管理了,事务自然就失效了。

解决方案:

需要保证每个事务注解的每个Bean被Spring管理。

8. 多线程

@Service
public class UserService { @Autowired
private UserMapper userMapper;
@Autowired
private RoleService roleService; @Transactional
public void add(UserModel userModel) throws Exception { userMapper.insertUser(userModel);
new Thread(() -> {
try {
test();
} catch (Exception e) {
roleService.doOtherThing();
}
}).start();
}
} @Service
public class RoleService { @Transactional
public void doOtherThing() {
try {
int i = 1/0;
System.out.println("保存role表数据");
}catch (Exception e) {
throw new RuntimeException();
}
}
}

我们可以看到事务方法add中,调用了事务方法doOtherThing,但是事务方法doOtherThing是在另外一个线程中调用的。

这样会导致两个方法不在同一个线程中,获取到的数据库连接不一样,从而是两个不同的事务。如果想doOtherThing方法中抛了异常,add方法也回滚是不可能的。

我们说的同一个事务,其实是指同一个数据库连接,只有拥有同一个数据库连接才能同时提交和回滚。如果在不同的线程,拿到的数据库连接肯定是不一样的,所以是不同的事务。

解决方案:

这里就有点分布式事务的感觉了,尽量还是保证在同一个事务中处理。

总结

本文简单阐述了下Spring中事务实现的原理,同时列举了8种Spring事务失败的场景,相信很多朋友可能都遇到过, 失败的原因也有详细说明。希望大家对Spring事务有一个新的认识。

欢迎关注微信公众号「JAVA旭阳」交流和学习

更多学习资料请移步:程序员成神之路

8个Spring事务失效的场景,你碰到过几种?的更多相关文章

  1. Spring事务Transactional和动态代理(三)-事务失效的场景

    系列文章索引: Spring事务Transactional和动态代理(一)-JDK代理实现 Spring事务Transactional和动态代理(二)-cglib动态代理 Spring事务Transa ...

  2. 就这?Spring 事务失效场景及解决方案

    小明:靓仔,我最近遇到了很邪门的事. 靓仔:哦?说来听听. 小明:上次看了你的文章<就这?一篇文章让你读懂 Spring 事务>,对事务有了详细的了解,但是在项目中还是遇到了问题,明明加了 ...

  3. Spring事务失效的 8 大原因,这次可以吊打面试官了!

    今天再来一篇<吊打面试官>系列,这次真的要吊打了,哈哈!(看往期吊打系列请在后台回复:吊打,我会陆续更新--) 前几天栈长不是发了一篇文章,里面有一个关于事务失效的问题: 用 Spring ...

  4. Spring事务失效的原因

    http://blog.csdn.net/paincupid/article/details/51822599 Spring事务失效的原因 5种大的原因 如使用mysql且引擎是MyISAM,则事务会 ...

  5. Spring事务失效的2种情况

    使用默认的事务处理方式 因为在java的设计中,它认为不继承RuntimeException的异常是”checkException”或普通异常,如IOException,这些异常在java语法中是要求 ...

  6. java面试记录二:spring加载流程、springmvc请求流程、spring事务失效、synchronized和volatile、JMM和JVM模型、二分查找的实现、垃圾收集器、控制台顺序打印ABC的三种线程实现

    注:部分答案引用网络文章 简答题 1.Spring项目启动后的加载流程 (1)使用spring框架的web项目,在tomcat下,是根据web.xml来启动的.web.xml中负责配置启动spring ...

  7. 聊聊spring事务失效的12种场景,太坑了

    前言 对于从事java开发工作的同学来说,spring的事务肯定再熟悉不过了. 在某些业务场景下,如果一个请求中,需要同时写入多张表的数据.为了保证操作的原子性(要么同时成功,要么同时失败),避免数据 ...

  8. spring事务失效的12种场景

    一 事务不生效 1.访问权限问题 java的访问权限主要有四种:private<default<protected<public. 把有某些事务方法,定义了错误的访问权限,就会导致事 ...

  9. spring事务失效情况分析

    详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt113 <!--[if !supportLists]-->一.&l ...

  10. Spring事务深入剖析--spring事务失效的原因

    之前我们讲的分布式事务的调用都是在一个service中的事务方法,去调用另外一个service中的业务方法, 如果在一个sevice中存在两个分布式事务方法,在一个seivice中两个事务方法相互嵌套 ...

随机推荐

  1. Java 读写锁 ReadWriteLock 原理与应用场景详解

    Java并发编程提供了读写锁,主要用于读多写少的场景,今天我就重点来讲解读写锁的底层实现原理@mikechen 什么是读写锁? 读写锁并不是JAVA所特有的读写锁(Readers-Writer Loc ...

  2. 改善C#程序的方法-1 操作字符串

    正确操作字符串 引言: 字符串是使用很频繁的一种数据类型. 如果使用不慎,则会为一次字符串操作所带来的额外性能开销而付出代价. 下面从这几个方面来探讨如何正确操作字符串: 1.确保尽量少的装箱,尽可能 ...

  3. 如何使用 Yolov4 训练人脸口罩检测模型

    前言 疫情当下,出入医院等公共场所都被要求佩戴口罩.这篇博客将会介绍如何使用 Yolov4,训练一个人脸口罩检测模型(使用 Yolov4 的原因是目前只复现到了 v4 ),代码地址为 https:// ...

  4. KMP模式匹配 学习笔记

    功能 能在线性时间内判断字符串\(A[1~N]\)是否为字符串\(B[1~M]\)的子串,并求出字符串\(A\)在字符串\(B\)中各次出现的位置. 实现 1.对字符串\(A\)进行自我"匹 ...

  5. MySQL安装卸载、idea中Database的使用、常用的sql语句

    MySQL安装卸载 MySQL安装 在下面的资源链接中下载MySQL软件压缩包(绿色版),这个版本是MySQL5.7.29的,本教程也只适用于这个绿色版的,如果下载的是安装包那就可能有些地方不一样了, ...

  6. GitLab CI/CD 自动化部署入门

    前言:因为找了B站内推,测试开发,正好知道内部使用GitLab做自动化测试,所以简单学了一下,有错误的地方请指正. 入门 初始化 cp: 无法获取'/root/node-v12.9.0-linux-x ...

  7. 网页头部的声明应该是用 lang="zh" 还是 lang="zh-CN"?

    网页头部的声明应该是用 lang="zh" 还是 lang="zh-CN"? 遇到问题 不知道大家有没有留意到一个问题,就是使用 VsCode 新建的 html ...

  8. photoshop 2021 for mac安装教程,亲测可用!!!

    小编分享下photoshop cc 2021 for mac 安装教程,适配M1芯片,让大家完美使用ps2021,畅享所有新功能Adobe Photoshop2021(简称PS) 新版本主要增加了Ne ...

  9. ssh登录提示hosts is down

    其他无用的网卡配置信息mv走重启network如果还是不行重启一下服务器问题就能解决

  10. awk变量

    awk变量 awk参数 参数 解释 -F 指定分割字段符 -v 定义或修改一个awk内部的变量 -f 从脚本文件中读取awk命令 对于awk而言,变量分为 内置变量 自定义变量 内置变量 解释 FS ...