Spring事务@Transactional失效的8大场景与解决方法
目录
- 1. 前言
- 2. @Transactional 核心参数解析
- 2.1 propagation (事务传播行为)
- 2.2 isolation(隔离级别)
- 2.3 rollbackFor / noRollbackFor(回滚规则)
- 2.4 timeout (超时时间)
- 2.5 readOnly(是否只读事务)
- 3. 事务失效的常见场景
- 3.1 方法不是 public
- 3.2 自调用(同类方法内部调用)
- 3.3 异常被捕获“吃掉”
- 3.4 异常类型不匹配
- 3.5 数据库引擎不支持事务
- 3.6 在异步方法中使用
- 3.7 未被Spring容器管理
- 3.8 propagation 参数设置错误
- 4. 结语
1. 前言
在我们开发Spring Boot
应用中,很多小伙伴以为只要在方法上加一个 @Transactional
,事务就能自动回滚,保证数据一致性。但实际开发中,事务经常出现失效的情况:明明抛了异常,数据库还是提交了。你肯定会疑惑:“为什么我加了注解,数据还是没回滚?”
通过本文博主将彻底和小伙伴们说清楚,让大家别再踩坑!从 @Transactional
的参数详解入手,再结合常见事务失效场景给出 正确写法 vs 错误写法
对比,帮助小伙伴们彻底理解 Spring
事务机制。
2. @Transactional 核心参数解析
Spring的 @Transactional
提供了很多参数,但日常最常用的主要有以下几个:
@Transactional( propagation = Propagation.REQUIRED, // 事务传播行为 isolation = Isolation.DEFAULTandroid, // 事务隔离级别 rollbackFor = Exception.class, // 回滚规则 timeout = 30, // 超时时间(秒) readOnly = false // 是否只读事务 )
2.1 propagation (事务传播行为)
作用:定义了当前事务方法与另一个事务方法相互调用时,事务应该如何传播。
Spring的传播属性有以下几种:
REQUIRED (默认):如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
假设现在有一个需求添加用户的时候给用户同时发放优惠券
用户Service
中userService.saveUser()
方法 调用 优惠券Service
的couponService.add()
方法。
当userService.saveUser()
运行时,先开启一个事务,此时couponService.add()
方法发现已经存在一个事务,就不会再开启事务,只要任何一个方法报错则都会回调。
REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起。
依旧以上述添加用户的时候给用户同时发放优惠券为例子
当userService.saveUser()
运行时,先开启一个事务A。当运行couponService.add()
时,把事务A挂起,然后开启事务B。就算事务A发生回滚,事务B依然能正常提交。
外部事务不会影响内部事务的提交和回滚。
- SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务方式继续运行。
- NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
- MANDATORY:必须在一个已有的事务中运行,否则抛出异常。
- NEVER:必须在非事务方式下运行,如果当前存在事务,则抛出异常。
- NESTED:如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则行为同 REQUIRED。
2.2 isolation(隔离级别)
作用:定义事务的隔离级别,解决并发事务可能带来的脏读、不可重复读、幻读等问题。
- READ_COMMITTED:避免脏读,但可能有不可重复读。
- REPEATABLE_READ:避免脏读和不可重复读。
- SERIALIZABLE:最高级别,完全串行化,性能差。
2.3 rollbackFor / noRollbackFor(回滚规则)
作用:指定哪些异常会触发事务回滚
默认:仅遇到运行时异常 RuntimeException
和 Error
才回滚
如果要对检查型异常(如 IOException)回滚,必须显式配置 rollbackFor = Exception.class
2.4 timeout (超时时间)
作用:事务的超时时间,单位为秒。如果超过该时间事务尚未完成,则自动回滚
2.5 readOnly(是否只读事务)
作用:提示数据库该事务为只读事务,数据库可能会进行一些优化。默认为false
3. 事务失效的常见场景
下面列出最常见的 8大事务php失效场景,并配合代码示例对比
3.1 方法不是 public
Spring
默认使用基于代理的 AOP
,对于非 public
方法,代理对象无法拦截到其调用,导致注解失效。
错误写法:
@Service public class UserService { @Transactional void saveUser(User user) { // 不是public userMapper.insert(user); throw new RuntimeException("模拟异常"); } }
正确写法:
@Service public class UserService { @Transactional public void saveUser(User user) { userMapper.insert(user); throw new RuntimeException("模拟异常"); } }
3.2 自调用(同类方法内部调用)
类内部的方法 A 调用另一个有 @Transactional
注解的方法 B,这属于自调用。调用的是 this 对象本身的方法,而不是被 Spring 代理增强过的对象的方法,因此事务不会生效。
错误写法:
@Service public class UserService { @Transactional public void addUser(User user) { userMapper.insert(user); throw new RuntimeException("模拟异常"); } public void process(User user) { // 自调用,绕过代理 this.addUser(user); } }
正确写法(通过代理调用):
@Service public class UserService { @Transactional public void addUser(User user) { userMapper.insert(user); throw new RuntimeException("模拟异常"); } } @Service public class OrderService { android @Autowired private UserService userService; public void process(User user) { // 通过Spring代理调用,事务才生效 userService.addUser(user); } }
3.3 异常被捕获“吃掉”
如果你在方法中捕获了异常,并且没有重新抛出,那么 Spring 的事务拦截器就感知不到异常,自然不会触发回滚
错误写法:
@Transactional public void saveUser(User user) { try { userMapper.insert(user); int i = 1 / 0; // 异常 } catch (Exception e) { System.out.println("异常被吃掉"); } }
正确写法(继续抛出):
@Transactional public void saveUser(User user) { try { userMapper.insert(user); int i = 1 / 0; } catch (Exception e) { throw new RuntimeException("抛出异常,保证回滚", e); } }
3.4 异常类型不匹配
默认情况下,只有 RuntimeException
和 Error
会触发回滚。如果你抛出了 IOException
, SQLException
等受检异常,事务依然会提交
错误写法:
@Transact编程客栈ional public void saveUser(User user) throws IOException { userMapper.insert(user); throw new IOException("检查型异常"); }
正确写法:
@Transactional(rollbackFor = Exception.class) public void saveUser(User user) throws IOException { userMapper.insert(user); throw new IOException("检查型异常"); }
3.5 数据库引擎不支持事务
以 mysql 为例:InnoDB 支持事务
而 MyISAM 不支持事务
如果表使用了 MyISAM
,即使 @Transactional
生效,也不会回滚
-- 检查并修改表引擎 SHOW TABLE STATUS LIKE ‘your_table_name'; ALTER TABLE your_table_name ENGINE = InnoDB;
3.6 在异步方法中使用
在 @Async
标记的异步方法中,操作是在一个新的线程中执行的。此时的事务上下文和新线程的事务上下文是不同的,需要配置特殊的事务管理器来支持,否则容易失效
// 需要特殊配置的场景 @Service public class AsyncService { @Async @Transactional // 普通配置下,此事务很可能失效 public void asyncTask() { // 异步事务操作 } }
3.7 未被Spring容器管理
这也是一些小伙伴粗心容易犯的错,类没有加上 @Service
, @Component
等注解,它就不会被 Spring 扫描并创建为 Bean,那么它上面的 @Transactional
注解自然无效
// 错误示例:只是一个普通类,不是Spring Bean // @Service 注释掉了 public class MyService { @Transactional // 这个注解完全没用 public void DOSomething() { // ... } }
3.8 propagation 参数设置错误
错误地设置传播行为可能导致意外情况。例如,在已经存在事务的方法中,以 NOT_SUPPORTED
或 NEVER
模式调用新方法
// 可能非预期的行为 @Service public androidclass MainService { @Transactional public void mainMethod() { // ... 主逻辑 subService.subMethod(); // 子方法挂起了当前事务 } } @Service public class SubService { @Transactional(propagation = Propagation.NOT_SUPPORTED) public void subMethod() { // 此方法以非事务方式运行,即使操作失败,也不影响mainMethod的事务 // 如果这里的操作需要原子性,那就出了问题 } }
4. 结语
小伙伴们通过本文的讲解,是否让你对Spring
事务@Transactional
注解有了一个更深的认识?实际上我们大部分开发场景都使用默认配置即可,同时建议都按照 @Transactional(rollbackFor = Exception.class)
进行注解,业务处理过程避免抛出受检异常IOException
, SQLException
等,导致失效!
到此这篇关于Spring事务@Transactional失效的8大场景与解决方法的文章就介绍到这了,更多相关Spring事务@Transactional失效内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!
精彩评论