spring事务管理之@Transactional
spring事务管理之@Transactional
- spring事务管理之@Transactional
- 1、概述
- 2、适用范围
- 3、核心属性
- propagation (传播行为) 深度解析:
- rollbackFor (回滚规则) 的关键点:
- 4、失效场景
- (1)作用的方法不是 public
- (2)同一个类中方法互相调用
- (3)异常被try-catch捕获
- (4)rollbackFor属性设置错误
- (5)数据库引擎不支持事务
- 5、补充场景
- 6、最佳实践
spring事务管理之@Transactional
1、概述
在Spring Boot项目开发过程中,我们往往会遇到这样的场景:一个方法里执行了多步数据库操作,但中间某一步出错,导致数据出现“半成功半失败”的情况。这样的数据不一致问题,可能会带来严重的业务风险。而 @Transactional
就是为了解决这个问题而生,它的核心作用就是 要么所有操作都成功,要么全部作废,保证数据库的完整性。
2、适用范围
-
类 (Class):当注解在类上时,表示该类所有 public 方法都应用相同的事务配置。
-
方法 (Method):当方法和类上都有注解时,方法级别的注解会覆盖类级别的配置。 这是更精细的控制方式。
-
接口 (Interface):不推荐使用。虽然技术上可行(如果使用 JDK 动态代理),但这是一种糟糕的实践。因为它会将事务这种实现细节耦合到接口定义中。如果切换到 CGLIB 代理(基于类的代理),接口上的注解会直接失效。
示例
@Transactionalpublic void createTask(TaskDetails taskDetails) {taskRepo.save(taskDetails);}
最佳实践:始终将 @Transactional 注解在具体的实现类或其方法上。
3、核心属性
属性 | 说明 | 常用值/重要说明 |
---|---|---|
propagation | 事务传播行为 (最重要)。定义当一个事务方法调用另一个事务方法时,事务如何交互。 | REQUIRED (默认), REQUIRES_NEW, NESTED |
isolation | 事务隔离级别。定义事务并发问题的处理能力(脏读、不可重复读、幻读)。 | READ_COMMITTED (常用), REPEATABLE_READ (MySQL默认) |
readOnly | 只读事务。设置为 true 可进行性能优化,数据库会跳过一些锁定和日志记录。 | true, false (默认) |
timeout | 事务超时 (秒)。超过指定时间未完成,事务将自动回滚。 | 整数值,如 30 |
rollbackFor | 指定回滚的异常。定义哪些异常类型会触发回滚。 | Exception.class (强烈推荐) |
noRollbackFor | 指定不回滚的异常。定义哪些异常类型不会触发回滚。 | 业务自定义的非关键异常 |
propagation (传播行为) 深度解析:
-
REQUIRED (默认):如果当前有事务,就加入;如果没有,就新建一个。这是最常见的场景。
-
REQUIRES_NEW:总是创建一个全新的、独立的事务。如果外部已有事务,会将其挂起。常用于需要独立提交的日志记录等场景。即使内部事务回滚,也不会影响外部事务。
-
NESTED:(注意:与REQUIRED不同!) 如果当前有事务,则创建一个嵌套事务(保存点 Savepoint)。嵌套事务是外部事务的一部分,它回滚不会影响外部事务。但如果外部事务回滚,嵌套事务也必须回滚。这需要数据库驱动支持保存点特性。
rollbackFor (回滚规则) 的关键点:
Spring 默认只在捕获到 RuntimeException
(未检查异常) 和 Error
时才会回滚事务。对于 Exception 的子类 (受检异常,如 IOException),默认是不会回滚的!这是一个巨大的“坑”。为了避免意外,养成良好习惯:
@Transactional(rollbackFor = Exception.class)
,这样可以确保任何异常都会触发回滚。
4、失效场景
(1)作用的方法不是 public
示例:
@Transactional
private void deleteTask(String taskId) { } // 此时事务不会生效!
Spring AOP中事务是通过代理机制实现的,而 JDK 动态代理只能代理 public 方法,因此其他访问级别的方法都不行。
(2)同一个类中方法互相调用
示例:
@Service
public class MyService {public void createTask() {// this 调用,绕过了代理对象,导致 methodB 的事务失效!this.generateTaskName(); }@Transactionalpublic void generateTaskName() {// ... 数据库操作 ...}
}
此时generateTaskName()
方法上的@Transactional
不会生效、因为this
关键字引用的是原始对象,而不是 Spring 创建的代理对象,即this.generateTaskName();直接调用了本类原始对象的方法,没有经过Spring代理。
正确的做法:
- 方案一
方法拆分到两个类中,通过@Autowired
注入进行调用。 - 方案二
通过AopContext.currentProxy()
获取当前方法的代理对象来调用,此时需要在启动类或配置类上加@EnableAspectJAutoProxy(exposeProxy = true)
。
@Service
public class MyService {public void createTask() {// 获取代理对象,通过代理对象调用((MyService) AopContext.currentProxy()).generateTaskName();}@Transactionalpublic void generateTaskName() {// ... 数据库操作 ...}
}
- 方案三
通过@Autowired
注入自己。
(3)异常被try-catch捕获
代理对象只能通过捕获从方法中抛出的异常来决定是否回滚。如果异常在方法内部被 catch,代理就无法感知到任何问题,会按正常流程提交事务。
@Transactionalpublic void createTask(TaskDetails taskDetails) {try {// operation...} catch (Exception e) {log.error("create task failed...", e);// 此时异常没被重新抛出,无法感知异常。}}
正确做法应该是catch住异常后重新抛出。
(4)rollbackFor属性设置错误
即方法抛出的异常不在rollbackFor指定的异常类型中,导致事务无法回滚。正确的做法是,若默认回滚策略没有期望回滚的异常类型,则添加rollbackFor指定具体的异常类型。
(5)数据库引擎不支持事务
事务的最终执行者是数据库。假设用的 MySQL 表引擎是 MyISAM,事务是不可能生效的,因为 MyISAM 根本不支持事务!要确保使用的存储引擎支持事务。
5、补充场景
通常,在Spring Data JPA中,repository.save()
方法会在事务提交时执行INSERT操作,此时如果违反唯一约束,异常会在事务提交时抛出。如果您在Service层的方法上使用了@Transactional
,那么异常会在Service方法结束后(事务提交时)抛出。因此,如果您在Service方法内部调用save
并尝试捕获,可能无法捕获到,因为事务提交发生在方法退出之后。
正确做法应该是:保持事务粒度最小化。事务方法应只包含必要的数据库操作,避免将耗时的、非事务性的操作放入其中,以免长时间占用数据库连接。并在事务之外对异常进行捕获。
6、最佳实践
- 确保事务方法是public,否则事务不生效;
- 避免同一类内部调用
@Transactional
方法,优选重构代码; - 异常要让Sprin感知,catch后抛出异常;
- 明确回滚规则:建议总是设置
rollbackFor = Exception.class
; - 善用只读事务:对于所有查询操作,添加
@Transactional(readOnly = true)
可以有效提升性能; - 保持事务粒度小:事务方法应只包含必要的数据库操作,避免将耗时的、非事务性的操作放入其中,以免长时间占用数据库连接。