Appearance
Spring 事务详解
很多人会用 @Transactional,但如果继续追问:它到底为什么能生效,什么时候会失效,传播行为又在说什么?
就很容易开始发虚。
这篇文章专门把 Spring 事务这条主线单独拉出来,重点讲:
- Spring 事务到底是什么
- 它解决什么问题
- 声明式事务为什么能工作
- 传播行为和隔离级别到底在控制什么
@Transactional常见失效场景有哪些
1. Spring 事务到底是什么
Spring 事务不是数据库事务本身,而是 Spring 对事务边界、传播行为、回滚规则和资源管理的一套统一抽象。数据库事务真正执行时,底层仍然依赖数据库能力;Spring 做的是把事务从零散的手动控制,变成统一、可声明、可治理的框架能力。
2. 它主要解决什么问题
如果没有 Spring 事务,业务代码经常会变成:
- 手动开启事务
- 调很多数据库操作
- 出错时手动回滚
- 最后手动提交
这种方式的问题非常明显:
- 样板代码多
- 事务边界容易混乱
- 出错时容易漏回滚
- 横切到很多业务方法里
Spring 事务要解决的核心问题就是:让事务边界以更统一、更声明式的方式表达出来。
3. 声明式事务和编程式事务有什么区别
3.1 声明式事务
这是最常见的方式。
例如:
java
@Transactional
public void createOrder() {
}它的重点是:你声明这里需要事务,具体开启、提交、回滚由框架处理。
例如一个更完整的订单示例:
java
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final StockService stockService;
public OrderService(OrderRepository orderRepository, StockService stockService) {
this.orderRepository = orderRepository;
this.stockService = stockService;
}
@Transactional
public void createOrder(CreateOrderCommand command) {
orderRepository.save(command.toOrder());
stockService.deduct(command.getProductId(), command.getCount());
}
}3.2 编程式事务
它更偏:由开发者自己控制事务开始、提交和回滚。
它更灵活,但侵入性更强。
例如更常见的工程写法是用 TransactionTemplate:
java
@Service
public class OrderApplicationService {
private final TransactionTemplate transactionTemplate;
private final OrderRepository orderRepository;
private final StockService stockService;
public OrderApplicationService(
TransactionTemplate transactionTemplate,
OrderRepository orderRepository,
StockService stockService) {
this.transactionTemplate = transactionTemplate;
this.orderRepository = orderRepository;
this.stockService = stockService;
}
/**
* createOrder
* 功能:通过编程式事务完成“保存订单 + 扣减库存”。
* 核心职责:
* 1. 显式声明事务边界
* 2. 在同一个事务里执行订单写入和库存扣减
* 3. 出现异常时主动标记回滚
* 参数:
* - command: 创建订单命令
* 返回值:
* - Long: 新订单 ID
*/
public Long createOrder(CreateOrderCommand command) {
return transactionTemplate.execute(status -> {
try {
Order order = command.toOrder();
orderRepository.save(order);
stockService.deduct(command.getProductId(), command.getCount());
return order.getId();
} catch (Exception exception) {
status.setRollbackOnly();
throw exception;
}
});
}
}这段代码里最值得注意的是:
- 事务边界不是由
@Transactional隐式声明,而是由transactionTemplate.execute(...)明确包起来 status.setRollbackOnly()表示当前事务应当回滚- 这类方式通常更适合“事务边界需要在代码里显式控制”的场景
它常见于:
- 需要在同一个方法里分段控制事务
- 事务逻辑不是很适合直接挂在某个公开方法上
- 希望把事务控制和业务分支判断写在一起
所以实际工程里更常见的,还是声明式事务。
4. @Transactional 为什么能工作
这条线一定要和 AOP 连起来看。
Spring 声明式事务通常依赖代理机制。也就是说:
- 业务方法本身不直接管理事务
- Spring 为目标对象创建代理
- 请求先进入代理
- 代理在方法前后织入事务开启、提交和回滚逻辑
所以 @Transactional 真正能生效,不是因为注解本身有魔法,而是因为方法调用经过了 Spring 代理对象。
4.1 @Transactional 常用属性
这部分在实际开发里很常见:
| 属性 | 作用 |
|---|---|
propagation | 指定传播行为 |
isolation | 指定隔离级别 |
rollbackFor | 指定哪些异常回滚 |
noRollbackFor | 指定哪些异常不回滚 |
readOnly | 声明只读事务 |
timeout | 指定超时时间 |
例如:
java
@Transactional(
propagation = Propagation.REQUIRED,
isolation = Isolation.READ_COMMITTED,
rollbackFor = Exception.class,
timeout = 5)
public void pay(Long orderId) {
}5. 事务边界到底在说什么
事务边界 可以理解成 哪些数据库操作应该被视为同一个整体,要么一起成功,要么一起失败。
例如创建订单时,可能同时涉及:
- 写订单表
- 扣库存
- 写支付记录
这时真正要先想清楚的是:这几步到底是不是一个事务整体。
事务问题首先是业务边界问题,然后才是框架注解问题。
例如下面这种写法就更接近合理事务边界:
java
@Transactional
public void payOrder(Long orderId) {
orderRepository.updateStatus(orderId, "PAID");
paymentRepository.savePaymentLog(orderId);
}但如果把远程调用、长时间等待也塞进事务里,就会把事务边界拉得过长:
java
@Transactional
public void payOrder(Long orderId) {
orderRepository.updateStatus(orderId, "PAYING");
remotePaymentClient.call(orderId);
Thread.sleep(3000);
paymentRepository.savePaymentLog(orderId);
}6. 传播行为到底在控制什么
传播行为最常见的场景是:一个已经在事务中的方法,又调用了另一个事务方法。
这时要回答的问题是:后进来的这个方法,到底加入当前事务,还是单独开新事务。
这就是传播行为。
6.1 REQUIRED:必须加入当前事务,若没有则新建
REQUIRED 可以理解成“需要事务就复用,没有事务就创建”。
它是最常见的传播行为,也是默认值。
含义是:
- 当前有事务,就加入当前事务
- 当前没有事务,就新建一个事务
一个更具体的 REQUIRED 示例
最常见的场景就是:下单方法里调用扣库存方法,而这两步本来就应该属于同一个事务整体。
java
@Service
public class OrderApplicationService {
private final OrderService orderService;
private final StockService stockService;
public OrderApplicationService(OrderService orderService, StockService stockService) {
this.orderService = orderService;
this.stockService = stockService;
}
@Transactional
public void createOrder(CreateOrderCommand command) {
orderService.saveOrder(command); // REQUIRED,加入当前事务
stockService.deductStock(command); // REQUIRED,加入当前事务
}
}java
@Service
public class StockService {
@Transactional(propagation = Propagation.REQUIRED)
public void deductStock(CreateOrderCommand command) {
// 扣库存
throw new IllegalStateException("库存不足");
}
}这段代码里,createOrder() 和 deductStock() 最终属于同一个事务。
所以执行结果是:
- 外层方法先保存订单
- 内层扣库存抛异常
- 整个事务一起回滚
- 订单数据和库存数据都不会提交
这就是 REQUIRED 最适合记住的印象:本来就是同一笔业务,就一起成,要么一起败。
6.2 REQUIRES_NEW:总是开启一个全新的事务
REQUIRES_NEW 可以理解成“挂起外层事务,自己单独开一个新事务”。
含义是:
- 不管外面有没有事务
- 自己都新开一个独立事务
一个更具体的 REQUIRES_NEW 示例
它特别常见于:主业务失败了,但审计日志、操作留痕、补偿记录仍然希望单独保存。
java
@Service
public class OrderFacade {
private final OrderService orderService;
private final AuditService auditService;
public OrderFacade(OrderService orderService, AuditService auditService) {
this.orderService = orderService;
this.auditService = auditService;
}
@Transactional
public void create(CreateOrderCommand command) {
orderService.saveOrder(command); // REQUIRED,加入外层事务
auditService.recordOperation(command); // REQUIRES_NEW,单独新开事务
throw new IllegalStateException("后续流程失败");
}
}java
@Service
public class AuditService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void recordOperation(CreateOrderCommand command) {
// 保存审计日志
}
}这段代码里,执行结果通常是:
- 外层
create()开启事务 recordOperation()被调用时,挂起外层事务- 审计日志在自己的新事务里提交成功
- 外层方法随后抛异常
- 外层订单事务回滚,但审计日志不会跟着回滚
所以 REQUIRES_NEW 最容易记住的一点是:外层失败,不等于内层一定失败;因为它们已经不是同一个事务了。
6.3 SUPPORTS:支持当前事务,但不强求必须有事务
SUPPORTS 可以理解成“有事务就跟着跑,没有事务就直接按非事务方式执行”。
含义是:
- 有事务就加入
- 没事务就按非事务方式执行
一个更具体的 SUPPORTS 示例
它更适合那种:如果外层本来就在事务里,那就顺手跟着;如果没有事务,也没必要为了它单独开事务。
例如查询订单详情和支付快照:
java
@Service
public class OrderQueryService {
private final OrderRepository orderRepository;
public OrderQueryService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
public OrderDetailDTO queryOrderDetail(Long orderId) {
return orderRepository.findDetailById(orderId);
}
}如果它被下面这个方法调用:
java
@Transactional
public void exportOrderReport(Long orderId) {
OrderDetailDTO detail = orderQueryService.queryOrderDetail(orderId);
reportRepository.save(buildReport(detail));
}那么 queryOrderDetail() 会加入当前事务。
但如果它被普通 Controller 直接调用:
java
@GetMapping("/orders/{id}")
public OrderDetailDTO detail(@PathVariable Long id) {
return orderQueryService.queryOrderDetail(id);
}那它就不会自己额外开事务,而是按普通非事务查询执行。
所以 SUPPORTS 更适合理解成:有现成事务就顺着用,没有也别为了它强行开一个。
6.4 一句话区分
可以直接记:
REQUIRED:优先加入当前事务REQUIRES_NEW:一定新开事务SUPPORTS:能跟就跟,不能跟就算了
6.5 一个传播行为示例
java
@Service
public class OrderFacade {
private final OrderService orderService;
private final AuditService auditService;
public OrderFacade(OrderService orderService, AuditService auditService) {
this.orderService = orderService;
this.auditService = auditService;
}
@Transactional
public void create(CreateOrderCommand command) {
orderService.saveOrder(command); // REQUIRED,加入当前事务
auditService.recordOperation(command); // REQUIRES_NEW,单独提交审计日志
}
}java
@Service
public class AuditService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void recordOperation(CreateOrderCommand command) {
}
}6.6 把 3 个最常见传播行为放在一起看
如果用同一个调用链去记忆,会更容易形成直觉:
| 场景 | 内层传播行为 | 内层和外层是不是同一个事务 | 外层失败时内层结果 |
|---|---|---|---|
| 下单 + 扣库存 | REQUIRED | 是 | 一起回滚 |
| 下单 + 记审计日志 | REQUIRES_NEW | 不是 | 审计日志可单独提交 |
| 导出报表时顺便查订单详情 | SUPPORTS | 有事务就加入,没有就不建 | 跟不跟事务取决于调用方 |
这张表真正想表达的是:
REQUIRED解决“本来就是一笔业务”REQUIRES_NEW解决“我想和外层拆开算”SUPPORTS解决“能跟就跟,但别强求”
7. 隔离级别到底在控制什么
隔离级别不是 Spring 发明的,它本质上还是数据库并发隔离策略。
Spring 在这里做的,是:把数据库隔离级别作为事务配置的一部分暴露出来。
它主要解决的是 多个事务并发执行时,彼此之间允许看到什么样的数据变化。
常见还是:
- 读未提交
- 读已提交
- 可重复读
- 串行化
如果把它们和并发读写里的几类典型异常放到一起看,会更直观:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
读未提交 READ UNCOMMITTED | ❌ 会发生 | ❌ 会发生 | ❌ 会发生 |
读已提交 READ COMMITTED | ✅ 不会 | ❌ 会发生 | ❌ 会发生 |
可重复读 REPEATABLE READ | ✅ 不会 | ✅ 不会 | ⚠️ InnoDB 下通常可避免 |
串行化 SERIALIZABLE | ✅ 不会 | ✅ 不会 | ✅ 不会 |
这张表真正想表达的是:
- 隔离级别越低,并发能力通常越高,但读到不稳定数据的概率也越大
- 隔离级别越高,一致性更强,但等待、锁竞争和吞吐代价通常也更明显
REPEATABLE READ在 MySQL InnoDB 里要结合MVCC、间隙锁、临键锁这些机制一起理解,不能只把它背成一句抽象定义
所以这里的隔离级别,本质上是在表达数据库并发隔离策略,而不是单独的 Spring 逻辑。
例如:
java
@Transactional(isolation = Isolation.REPEATABLE_READ)
public Order findForReport(Long orderId) {
return orderRepository.findById(orderId);
}8. 什么情况下会回滚
这也是事务里特别容易误解的一点。
很多人会简单理解成:只要报错就回滚。
但回滚行为实际上要看:
- 抛出的异常类型
- 异常是否真的抛出到事务代理层
- 是否配置了回滚规则
所以事务回滚不是一句“报错就行”,而是:异常要真正被事务机制感知到。
8.1 回滚规则示例
java
@Transactional(rollbackFor = Exception.class)
public void importData() throws Exception {
if (true) {
throw new Exception("import failed");
}
}如果你希望某些业务异常不要回滚,也可以显式指定:
java
@Transactional(noRollbackFor = BizWarningException.class)
public void notifyUser() {
throw new BizWarningException("ignore rollback");
}9. @Transactional 常见失效场景有哪些
这是工程里特别高频的坑。
9.1 同类内部自调用
这是最经典的一类。
原因是:同类内部直接调用方法,往往不会经过 Spring 代理对象。
没有经过代理,事务增强就没机会执行。
例如:
java
@Service
public class UserService {
public void register() {
saveUser(); // 同类内部直接调用,可能不会走事务代理
}
@Transactional
public void saveUser() {
}
}9.2 方法没有被 Spring 容器管理
如果对象本身都不在容器里,事务自然也无从增强。
9.3 方法可见性或调用方式不合适
声明式事务是基于代理增强的,所以某些调用路径或可见性条件下,可能无法按预期生效。
9.4 异常被吞掉
如果业务代码内部把异常吃掉了,没有正确抛出到事务层,预期的回滚就可能不发生。
例如:
java
@Transactional
public void createOrder() {
try {
orderRepository.save(new Order());
throw new RuntimeException("fail");
} catch (Exception ex) {
// 异常被吞掉,事务层可能感知不到
log.warn("ignore", ex);
}
}9.5 以为加了注解就自动把所有下游都纳入同一事务
事务边界是有实际资源范围的,不是加个注解就跨系统自动统一了。
10. Spring 事务和数据库事务是什么关系
一定要把这层边界讲清楚。
可以直接记成:
- Spring 事务负责统一事务表达和控制方式
- 数据库事务负责真正的提交、回滚和隔离
所以 Spring 事务不是替代数据库事务,而是站在应用框架层,对数据库事务做统一组织。
11. 为什么长事务会非常麻烦
长事务带来的问题不只是“执行慢”,还包括:
- 长时间占用数据库连接
- 长时间持有锁
- 放大并发冲突
- 拖慢连接池和系统吞吐
所以工程上一个很重要的原则是:事务边界尽量短,只包真正需要一起提交回滚的数据库操作。
12. 工程上最常见的几个误区
12.1 误区一:加了 @Transactional 就一定安全
事务是否真正生效,要看调用路径和异常传播,不是只看注解写没写。
12.2 误区二:事务能解决所有一致性问题
事务主要解决单个事务边界内的一致性问题,不等于自动解决跨服务、跨消息链路的一致性。
12.3 误区三:事务越大越稳
事务越大,往往锁持有越久、连接占用越久,反而更容易把系统拖慢。
12.4 误区四:传播行为只是背几个枚举
传播行为真正要回答的是业务边界问题,而不是只背注解参数。
13. 一句话总结
Spring 事务的核心价值,是把数据库事务这件事以更统一、更声明式的方式纳入应用框架管理;而真正要把它用好,关键仍然在于事务边界、传播行为、异常传播和调用路径这几件事是否想清楚了。