Spring 声明式事务:原理、使用及失效场景详解
一、事务的基础概念
首先要明确,事务(Transaction) 是数据库操作的最小工作单元,它保证了一组操作要么全部成功执行,要么全部失败回滚,核心遵循 ACID 原则:
原子性(Atomicity):事务是不可分割的整体,操作要么全做,要么全不做。
一致性(Consistency):事务执行前后,数据库的完整性约束不被破坏(比如转账后总金额不变)。
隔离性(Isolation):多个事务并发执行时,彼此互不干扰(避免脏读、幻读、不可重复读)。
持久性(Durability):事务提交后,修改会永久保存到数据库,不会因故障丢失。
Spring 事务的核心价值是简化事务管理:无需手动编写开启、提交、回滚事务的代码,通过声明式注解即可实现,降低了业务代码与事务管理的耦合。
为了便于理解简单举个例子:
用餐厅点餐结账这个场景,你走进一家餐厅,点了鱼香肉丝、宫保鸡丁和一碗米饭,打算吃完这三道菜再一起结账 ------ 这个 "点三道菜 + 吃 + 结账" 的完整过程,就是一个事务。
原子性:要么三道菜全上齐,你吃完开开心心付完钱走;要么只要有一道菜做不出来(比如没鸡肉了),整个流程就作废 ------ 你一道菜都不吃,也一分钱不付。绝对不会出现 "只上两道菜,却让你付三道菜钱" 的中间情况,事务的操作是 "打包" 的,要么全成,要么全败。
一致性:结账前你兜里有 200 块,餐厅收银台里有 1000 块,总共 1200 块;你花 100 块结账后,你兜里剩 100 块,收银台变成 1100 块,加起来还是 1200 块。总金额没有因为这次消费凭空变多或变少,这就是 "数据的完整性没被破坏"。要是结账后你剩 100,收银台只多了 50,那就是违背了一致性 ------ 钱平白少了 50,肯定出问题了。
隔离性:你旁边桌也有客人在点餐结账,你们俩的账单是完全分开的。他不会替你付鱼香肉丝的钱,你也不会多付他那碗汤的钱。就像数据库里同时跑着好几个事务,它们之间互相 "看不见",不会互相干扰,各办各的事。
持久性:你付完钱后,服务员把你的消费记录存进了系统,还打印了小票给你。就算这时候餐厅突然停电、收银系统崩溃,你的消费记录也不会消失 ------ 系统修好后一查,还是能看到你付了 100 块。这就是事务提交后,数据的修改会被永久保存,不会因为任何意外丢失。
来举点反例:
如果违背 ACID 原则会怎么样?
违背原子性:厨房只做了鱼香肉丝,没做宫保鸡丁和米饭,但你还是付了 3 道菜的钱(流程只走了一半);
违背一致性:你付了 100 元,但收银台只多了 50 元(总金额对不上了);
违背隔离性:你旁边桌的客人没付钱,却用了你的账单(两个事务互相干扰);
违背持久性:你付完钱后,餐厅停电,收银系统把你的消费记录删了(钱付了但没记录)。
二、Spring 事务的两种实现方式
Spring 支持两种事务管理方式,其中声明式事务是日常开发的主流。
1. 编程式事务(手动控制)
通过编写代码手动管理事务的开启、提交、回滚,灵活性高但侵入性强,适合特殊定制化场景。
java
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
@Service
public class UserService {
@Autowired
private DataSourceTransactionManager transactionManager;
@Autowired
private JdbcTemplate jdbcTemplate;
public void transferMoney() {
// 1. 定义事务属性(默认传播行为、隔离级别)
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
// 2. 开启事务
TransactionStatus status = transactionManager.getTransaction(def);
try {
// 业务操作:转账(扣减A账户,增加B账户)
jdbcTemplate.update("UPDATE user SET money = money - 100 WHERE id = 1");
jdbcTemplate.update("UPDATE user SET money = money + 100 WHERE id = 2");
// 3. 提交事务
transactionManager.commit(status);
} catch (Exception e) {
// 4. 异常时回滚事务
transactionManager.rollback(status);
throw new RuntimeException("转账失败", e);
}
}
}
2. 声明式事务(注解式,推荐)
通过@Transactional注解声明事务规则,Spring 自动完成事务的开启、提交 / 回滚,无侵入性。
(1)基础使用
java
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
// 标注该方法开启事务
@Transactional
public void transferMoney() {
// 业务操作:任意一步失败,整个事务回滚
jdbcTemplate.update("UPDATE user SET money = money - 100 WHERE id = 1");
// 模拟异常:触发事务回滚
// int i = 1 / 0;
jdbcTemplate.update("UPDATE user SET money = money + 100 WHERE id = 2");
}
}
(2)核心属性(常用)
@Transactional的关键属性可定制事务行为,核心属性如下:
| 属性名 | 作用 | 常用值 |
|---|---|---|
propagation |
事务传播行为(定义方法调用时事务的传递规则) | REQUIRED(默认,无事务则新建,有则加入)、REQUIRES_NEW(新建独立事务,挂起当前事务) |
isolation |
事务隔离级别(解决并发问题) | DEFAULT(默认,使用数据库隔离级别)、READ_COMMITTED(读已提交,避免脏读) |
rollbackFor |
指定触发回滚的异常类型(默认仅回滚 RuntimeException) | Exception.class(所有异常都回滚) |
readOnly |
是否只读事务(优化性能,只读操作建议设为 true) | true/false(默认 false) |
timeout |
事务超时时间(秒),超时则回滚 | 如5(5 秒超时) |
(3)示例:定制事务属性
java
// 所有Exception都回滚,隔离级别读已提交,超时5秒,只读(仅查询时用)
@Transactional(rollbackFor = Exception.class, isolation = Isolation.READ_COMMITTED, timeout = 5, readOnly = false)
public void transferMoney() {
// 业务逻辑
}
三、Spring 事务的核心原理
Spring 事务的底层是AOP(面向切面编程):
- Spring 扫描到
@Transactional注解后,会为目标类创建动态代理对象; - 当调用标注了
@Transactional的方法时,代理对象先执行事务增强逻辑(开启事务); - 执行目标方法的业务逻辑;
- 如果方法正常结束,代理对象提交事务;如果抛出指定异常,代理对象回滚事务。
注意:
@Transactional生效的前提:注解标注在public 方法上(private/protected 方法不生效,因为 Spring AOP 基于动态代理,无法拦截非公有方法);
异常未被方法内部
try-catch吞掉(如果手动捕获异常不抛出,Spring 无法感知,事务不会回滚)。
四、常见的Spring 事务失效场景(面试常问)
Spring 事务基于动态代理实现,只有代理对象触发的方法调用才会走事务增强逻辑。一旦破坏了代理的执行链路,或者违背了事务注解的生效规则,事务就会失效。
1. 注解标注在非 public 方法上
失效原因 :Spring AOP 动态代理只能拦截public方法,private/protected/ 默认(package-private)方法上的@Transactional会被忽略,事务不生效。
示例(错误):
java
@Service
public class UserService {
// private方法,事务失效
@Transactional
private void transferMoney() {
// 业务逻辑
}
}
解决方案 :将注解标注在public方法上。
2. 方法内部自调用(最常见)
失效原因:同一个类中,非事务方法调用事务方法,不会经过代理对象,而是直接调用目标方法,事务增强逻辑无法触发。
示例(错误):
java
@Service
public class UserService {
// 非事务方法
public void outerMethod() {
// 内部调用事务方法,事务失效
this.transferMoney();
}
@Transactional
public void transferMoney() {
// 业务逻辑
}
}
解决方案:
方案 1:将两个方法拆到不同的类中,通过依赖注入调用(走代理);
方案 2:通过 Spring 上下文获取当前类的代理对象调用:
java
@Service
public class UserService implements ApplicationContextAware {
private ApplicationContext context;
private UserService proxySelf;
@PostConstruct
public void init() {
// 获取当前类的代理对象
proxySelf = context.getBean(UserService.class);
}
public void outerMethod() {
// 用代理对象调用,事务生效
proxySelf.transferMoney();
}
@Transactional
public void transferMoney() {
// 业务逻辑
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.context = applicationContext;
}
}
3. 异常被手动捕获且未抛出
失效原因 :Spring 事务通过感知方法抛出的异常来触发回滚,如果异常被try-catch吞掉且不重新抛出,Spring 无法感知异常,事务会正常提交。
示例(错误):
java
@Service
public class UserService {
@Transactional
public void transferMoney() {
try {
// 业务操作(如转账)
int i = 1 / 0;x // 模拟异常
} catch (Exception e) {
// 捕获异常但不抛出,事务不会回滚
System.out.println("操作失败");
}
}
}
解决方案:捕获异常后重新抛出,让 Spring 感知:
java
@Transactional
public void transferMoney() {
try {
int i = 1 / 0;
} catch (Exception e) {
System.out.println("操作失败");
throw new RuntimeException(e); // 重新抛出异常
}
}
4. 注解配置的回滚异常类型不匹配
失效原因 :@Transactional默认只对RuntimeException(运行时异常)和Error回滚,对Checked Exception(如IOException)不回滚,若业务抛出此类异常,事务不会回滚。
示例(错误):
java
@Service
public class UserService {
@Transactional
public void transferMoney() throws IOException {
// 业务操作
throw new IOException("IO异常"); // 非运行时异常,事务不回滚
}
}
解决方案 :通过rollbackFor指定回滚的异常类型:
java
// 指定所有Exception都回滚
@Transactional(rollbackFor = Exception.class)
public void transferMoney() throws IOException {
throw new IOException("IO异常"); // 事务会回滚
}
5. 数据源未配置事务管理器
失效原因 :Spring 需要明确配置PlatformTransactionManager(如DataSourceTransactionManager)来管理事务,若未配置,注解会失效。
示例(缺失配置):
java
// 仅配置数据源,未配置事务管理器
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() {
// 配置数据源
return new DruidDataSource();
}
}
解决方案:添加事务管理器配置:
java
@Configuration
public class TransactionConfig {
@Autowired
private DataSource dataSource;
@Bean
public PlatformTransactionManager transactionManager() {
// 配置数据源事务管理器
return new DataSourceTransactionManager(dataSource);
}
}
6. 传播行为配置错误
失效原因 :若事务传播行为配置为SUPPORTS/NOT_SUPPORTED/NEVER等,在无外层事务的情况下,当前方法不会创建事务,导致事务失效。
示例(错误):
java
@Service
public class UserService {
// SUPPORTS:有事务则加入,无则不创建
@Transactional(propagation = Propagation.SUPPORTS)
public void transferMoney() {
// 无外层事务时,当前方法无事务,操作失败不会回滚
}
}
解决方案 :使用默认的REQUIRED(无事务则新建,有则加入),或根据业务需求选择REQUIRES_NEW:
java
@Transactional(propagation = Propagation.REQUIRED) // 默认值,推荐
public void transferMoney() {
// 业务逻辑
}
7. 类未被 Spring 容器管理
失效原因 :@Transactional仅对 Spring 容器中的 Bean 生效,若类未加@Service/@Component等注解,或通过new手动创建实例,事务失效。
示例(错误):
java
// 未加@Service,不是Spring Bean
public class UserService {
@Transactional
public void transferMoney() {
// 事务失效
}
}
// 手动new实例,不走Spring代理
public class Test {
public static void main(String[] args) {
UserService service = new UserService();
service.transferMoney(); // 事务失效
}
}
解决方案 :给类加@Service/@Component,通过 Spring 容器获取实例(如@Autowired注入)。
8.快速排查事务失效的技巧
- 先检查方法是否为
public,类是否被 Spring 管理; - 检查是否有内部自调用,或异常被
try-catch吞掉; - 查看
@Transactional的rollbackFor和propagation配置是否合理; - 日志排查:开启 Spring 事务日志,查看是否有 "Creating transaction""Rolling back transaction" 等关键日志。
总结
Spring 事务核心是简化数据库事务管理,遵循 ACID 原则,分为编程式(手动控制)和声明式(注解@Transactional)两种方式,声明式是主流;
@Transactional的核心属性可定制事务行为,重点关注propagation(传播行为)、rollbackFor(回滚异常)、readOnly(只读优化);
Spring 事务基于 AOP 动态代理实现,需注意注解标注在 public 方法、异常需抛出才能触发回滚。
补:
Spring声明式事务执行流程:
否
是
否
是
是
否
客户端调用业务方法
Spring 动态代理对象拦截请求
方法是否标注 @Transactional?
直接执行目标业务方法
解析注解属性:传播行为/隔离级别/回滚规则等
根据传播行为处理事务
-
无事务:新建事务
-
有事务:加入已有事务
执行目标业务方法核心逻辑
方法执行是否抛出异常?
提交事务
异常是否匹配 rollbackFor?
事务回滚
不回滚事务,正常提交
返回方法执行结果给客户端