前言

作为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.rollbackconnection.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 {@Autowiredprivate 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 somethingdoInsert();}@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 {@Autowiredprivate UserMapper userMapper;@Autowiredprivate AddressMapper addressMapper;@Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)public  void doInsert(User user,Address address) throws Exception {//do somethinguserMapper.insert(user);saveAddress(address);}@Transactional(propagation = Propagation.REQUIRES_NEW)public  void saveAddress(Address address) {//do somethingaddressMapper.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 {@Transactionalpublic void updateOrder(Order order) {// update order}
}

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

解决方案:

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

8. 多线程

@Service
public class UserService {@Autowiredprivate UserMapper userMapper;@Autowiredprivate RoleService roleService;@Transactionalpublic void add(UserModel userModel) throws Exception {userMapper.insertUser(userModel);new Thread(() -> {try {test();} catch (Exception e) {roleService.doOtherThing();}}).start();}
}@Service
public class RoleService {@Transactionalpublic 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事务有一个新的认识。

8个Spring事务失效的场景,你碰到过几种?相关推荐

  1. 面试官:Spring事务失效的场景有哪些?如何解决?

     作者:溪~源 blog.csdn.net/xuan_lu/article/details/107797505 实际项目开发中,如果涉及到多张表操作时,为了保证业务数据的一致性,大家一般都会采用事务机 ...

  2. Spring事务失效常见场景

    一.事务方法访问修饰符非public,导致事务失效 1.实例 2.解决 方式一:将方法修饰符改为public 方式二:开启AspectJ代理模式 3.注意 如果事务是static.final的,同样无 ...

  3. 详细整理Spring事务失效的具体场景及解决方案

    实际项目开发中,如果涉及到多张表操作时,为了保证业务数据的一致性,大家一般都会采用事务机制:好多小伙伴可能只是简单了解一下,遇到事务失效的情况,便会无从下手,溪源此篇文章给大家整理了一下常见Sprin ...

  4. 你否有遇到Spring事务失效,花费太多时间找bug

    作者:溪~源 来源:blog.csdn.net/xuan_lu/article/details/107797505 实际项目开发中,如果涉及到多张表操作时,为了保证业务数据的一致性,大家一般都会采用事 ...

  5. 详解spring事务失效和回滚失败的场景

    详解spring事务失效和回滚失败的场景 详解spring事务失效和回滚失败的场景 前言 一 .事务不生效 1.访问权限问题 2. 方法用final修饰 3.方法的内部调用 3.1 新加一个Servi ...

  6. Spring 事务失效?看这篇文章就够了!

    欢迎关注方志朋的博客,回复"666"获面试宝典 用 Spring 的 @Transactional 注解控制事务有哪些不生效的场景? 不知道小伙伴们有没有这样的经历,在自己开心的编 ...

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

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

  8. 面试官:你知道哪几种事务失效的场景?

    前言 声明式事务是Spring功能中最爽之一,可是有些时候,我们在使用声明式事务并未生效,这是为什么呢? 今天陈某带大家来聊一聊声明事务的几种失效场景.本文将会从以下两个方面来说一下事务为什么会失效? ...

  9. Spring 事务失效的 8 大场景,面试官直呼666...

    前几天发了一篇文章里面有一个关于事务失效的问题: 用 Spring 的 @Transactional 注解控制事务有哪些不生效的场景? 其中有个热心粉丝留言分享了下,我觉得总结得有点经验,给置顶了: ...

最新文章

  1. Hibernate的DetachedCriteria使用(含Criteria)
  2. 响应式注意要添加“视口”约束标记---viewport
  3. 自学python之路(day2)
  4. 菜鸟教程:C++ 的关键字(保留字)完整介绍
  5. Swoole找不到telnet命令
  6. LeetCode题解之Copy List with Random Pointer
  7. 2017年4月19日
  8. FFMPEG av_frame_move_ref 使用
  9. 数列分段II(信息学奥赛一本通-T1436)
  10. linuxdeploy ubuntu 更新gui_在Ubuntu和Linux Mint上释放空间的7种简单方法
  11. 计算机科学,大一学生怎样来爱你(文PPT)
  12. 风险模型—CreditMetrics模型1
  13. cs服务器搭建(cobaltstrike)
  14. IMU与Gyro及其它相关
  15. Base理论是什么?之前也聊到过CAP理论
  16. Mac sudo: no valid sudoers sources found, quitting
  17. 沈阳航空航天大学计算机考研真题知识点摘要
  18. iwnpi 5621ds RF测试指令
  19. 济南服务器空间_济南虚拟主机
  20. 【Flutter -- 基础组件】Flutter 导航栏

热门文章

  1. Web Cache原理,你真的造吗?
  2. 使用 you-get 下载视频
  3. Python之获取每日涨停板股票数据并绘制K线图!
  4. Lintcode 428. Pow(x, n) (Medium) (Python)
  5. vue中获取获取当前城市地址_Vue中获取当前定位城市名
  6. 对象和函数对象有什么区别?
  7. access是用来干什么的_Access是干嘛的,可以用来干什么?
  8. 如何测量无人机电机和螺旋桨的效率?
  9. 为你的NAS开启DDNS服务,独立IP IPV6 可用 - DDNS-GO
  10. 微软大中华区正式公告:陈永正卸任CEO 张亚勤过渡