前言

作为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. windows C++ call ADB command

    提供两种方式: 1.Windows API 2.Windows _popen // ADBHelper.cpp : This file contains the 'main' function. Pr ...

  2. CAN随机度测试

    在车联网安全测试场景中必不可少的就是对于CAN总线的测试,而CAN总线测试中27服务又是一项必不可少的测试! 当我们进入编程会话请求种子后必定会对种子进行破解,当种子随机度足够安全时就会加大破解难度. ...

  3. 关于IOC容器

    1.什么是 IOC (1)控制反转,把对象创建和对象之间的调用过程,交给 Spring 进行管理 (2)使用 IOC 目的:为了耦合度降低

  4. winscp报错Server sent passive reply with unroutable address. Using server address instead

    找了一堆没用. 最后终于 1.使用winSCP连接ftp时,编辑会话,单击高级. 2.进入高级设置之后,单击连接,查看连接模式,把被动模式的勾,勾掉. 3.单击确定,然后保存配置,重新连接FTP,OK

  5. LeetCode------找到所有数组中消失的数字(6)【数组】

    来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/find-all-numbers-disappeared-in-an-array 1.题目 找到 ...

  6. 齐博x1如何取消某个标签的缓存时间

    标签默认会有缓存, 如果你要强制取消缓存时间的话, 可以加上下面的参数 time="-1"如下图所示 标签默认缓存时间是10分钟, 你也可以改成其它时间 比如 time=" ...

  7. NLP之Bi-LSTM(在长句中预测下一个单词)

    Bi-LSTM @ 目录 Bi-LSTM 1.理论 1.1 基本模型 1.2 Bi-LSTM的特点 2.实验 2.1 实验步骤 2.2 实验模型 1.理论 1.1 基本模型 Bi-LSTM模型分为2个 ...

  8. 9.为url添加可选的后缀

    为url添加可选的后缀 在drf的机制中,响应数据的格式不再与单一内容类型连接,可以同时享有json格式或html格式,我们可以为api路径添加格式后缀的支持,使用格式后缀给我们明确指定了给定格式的u ...

  9. JavaSPI详解

    目录 一个问题 什么是SPI API 与 SPI 一个简单的例子 SPI机制的实现 Java SPI的问题 为什么SPI机制打破了双亲委派模型 参考资料 一个问题 在项目开发中,经常会使用到数据库驱动 ...

  10. vue-axios增加操作

    <template> <div class="Insert"> <label for="name">名称:</labe ...