有个 service 方法标了 @Transactional,跑测试也好好的,上生产之后出现部分写入 —— 余额扣了但订单没创建,或者订单创了但日志没写。Spring 事务"看起来生效但实际没"是 Java 工程师每年都要踩一遍的坑。本文把我们这两年遇到的 7 种失效场景写下来,每个附复现代码 + 正确写法。
前置:Spring @Transactional 的本质
Spring 的 @Transactional 是AOP 代理实现:
- Spring 启动时扫描所有
@Transactional的 bean - 给这个 bean 生成一个代理对象(JDK 动态代理或 CGLIB 子类)
- 代理对象在方法调用前后开启 / 提交 / 回滚事务
- 注入到其他 bean 的是代理对象,不是原对象
所有失效场景的根源都是:方法调用没经过代理对象。
失效 1:同类内部方法调用
@Service
public class OrderService {
public void create(Order order) {
save(order);
deductInventory(order);
}
@Transactional
public void save(Order order) { // ← 这个 @Transactional 不会生效
orderRepo.save(order);
// 抛异常时,期望回滚,但实际不会
if (order.getAmount() < 0) {
throw new IllegalArgumentException();
}
}
}
外部调用 create():Spring 代理拦截到了 create,但 create 里 save() 是用 this.save() 调用的 —— 这个 this 是原对象,不是代理对象,根本没经过 AOP 拦截器,事务注解失效。
修法 1:把方法拆到另一个 bean
@Service
public class OrderService {
@Autowired private OrderTransactionalService txService;
public void create(Order order) {
txService.save(order); // 跨 bean 调用,走代理
txService.deductInventory(order);
}
}
@Service
public class OrderTransactionalService {
@Transactional
public void save(Order order) { ... }
@Transactional
public void deductInventory(Order order) { ... }
}
修法 2:注入自己的代理
@Service
public class OrderService {
@Autowired private OrderService self; // 注入自己的代理(同一个 bean,但是是代理对象)
public void create(Order order) {
self.save(order); // 调用代理,事务生效
}
@Transactional
public void save(Order order) { ... }
}
修法 3:用 AopContext 拿当前代理
@EnableAspectJAutoProxy(exposeProxy = true) // 启动类加这个
public class App { ... }
@Service
public class OrderService {
public void create(Order order) {
((OrderService) AopContext.currentProxy()).save(order);
}
@Transactional
public void save(Order order) { ... }
}
失效 2:方法不是 public
@Service
public class OrderService {
@Transactional
private void save(Order order) { // ← private 方法事务不生效
orderRepo.save(order);
}
@Transactional
protected void deduct(Long uid) { // ← protected 也不生效
// ...
}
@Transactional
void process(Order order) { // ← package private 不生效
// ...
}
}
Spring AOP 的默认实现(JDK 代理 + CGLIB)只能拦截 public 方法。private 方法编译期就被链接,代理拦不到。protected 和 package-private 同样不行(虽然 CGLIB 技术上能但 Spring 默认不拦)。
修法:把方法改成 public,或者用 AspectJ 编译期织入(项目里很少这么用)。
失效 3:异常被吞掉
@Service
public class OrderService {
@Transactional
public void create(Order order) {
orderRepo.save(order);
try {
inventoryService.deduct(order); // 抛 BusinessException
} catch (Exception e) {
log.error("deduct failed", e);
// 异常被吞,事务不会回滚,订单已经入库
}
}
}
Spring 事务回滚靠抛出未捕获异常。catch 住等于告诉 Spring "一切正常",事务提交。
修法:
@Transactional
public void create(Order order) {
orderRepo.save(order);
try {
inventoryService.deduct(order);
} catch (BusinessException e) {
log.error("deduct failed", e);
// 主动标记当前事务回滚
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
// 或者重新抛
throw e;
}
}
失效 4:抛 checked exception 但没声明 rollbackFor
@Transactional
public void create(Order order) throws IOException { // checked exception
orderRepo.save(order);
sendNotification(order); // 抛 IOException
}
Spring 默认只对 RuntimeException 和 Error 回滚!checked exception(IOException / SQLException 等)默认不回滚。
修法:
@Transactional(rollbackFor = Exception.class) // 对所有异常回滚
public void create(Order order) throws IOException {
orderRepo.save(order);
sendNotification(order);
}
// 或者只针对特定异常
@Transactional(rollbackFor = {IOException.class, BusinessException.class})
public void create(Order order) { ... }
团队规范:所有 @Transactional 一律加 rollbackFor = Exception.class,避免漏。
失效 5:数据库引擎不支持事务
-- MyISAM 不支持事务,所有事务声明都是无效的
CREATE TABLE orders (
id INT PRIMARY KEY,
amount DECIMAL(10,2)
) ENGINE=MyISAM; -- ← 这就是坑
-- 必须用 InnoDB
CREATE TABLE orders (
id INT PRIMARY KEY,
amount DECIMAL(10,2)
) ENGINE=InnoDB;
这年头 MyISAM 已经罕见,但 H2 测试库 / 部分内存数据库默认不开事务支持。
失效 6:多数据源切换
@Transactional
public void crossDb() {
primaryRepo.save(orderA); // 主库:DataSource 1
secondaryRepo.save(orderB); // 从库:DataSource 2
// Spring 默认单 DataSource 事务,跨数据源不能保证一致
}
修法:
- 避免跨库写:架构层面解决最稳
- 用 JTA 分布式事务:Atomikos / Narayana,性能很差
- Saga 模式:每库本地事务 + 失败补偿,业务复杂但性能好
// Spring 多数据源:不同方法用不同事务管理器
@Transactional(transactionManager = "primaryTxManager")
public void saveToA(Order o) { ... }
@Transactional(transactionManager = "secondaryTxManager")
public void saveToB(Order o) { ... }
// 跨库一致性自己保证(消息补偿 / Saga)
public void crossDbSaga(Order a, Order b) {
try {
saveToA(a);
} catch (Exception e) {
return;
}
try {
saveToB(b);
} catch (Exception e) {
revertA(a); // 补偿
}
}
失效 7:Propagation 配错
@Service
public class OuterService {
@Autowired private InnerService inner;
@Transactional
public void outer() {
inner.inner(); // 期望加入外层事务
}
}
@Service
public class InnerService {
@Transactional(propagation = Propagation.REQUIRES_NEW) // 新开独立事务
public void inner() {
// ...
}
}
REQUIRES_NEW 让 inner 开新事务,outer 暂停。如果 outer 抛异常,outer 回滚,但 inner 已经提交。可能不是你想要的。
7 种 Propagation 简要:
REQUIRED 默认。有就加入,没就新建 ← 绝大多数情况
REQUIRES_NEW 永远新建独立事务 ← 日志记录、强制提交场景
SUPPORTS 有就加入,没就非事务执行
NOT_SUPPORTED 永远非事务执行(挂起外层)
MANDATORY 必须有外层,没有抛异常
NEVER 必须没外层,有就抛异常
NESTED 嵌套事务(SavePoint)
调试事务的方法
# application.properties 开 Spring 事务日志
logging.level.org.springframework.transaction=DEBUG
logging.level.org.springframework.orm.jpa=DEBUG
# 看 SQL 实际执行
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
# 正常事务日志输出:
[main] DEBUG o.s.t.i.TransactionInterceptor - Getting transaction for [OrderService.create]
[main] DEBUG o.h.SQL - INSERT INTO orders ...
[main] DEBUG o.s.t.i.TransactionInterceptor - Completing transaction for [OrderService.create]
[main] DEBUG o.s.t.i.TransactionInterceptor - Transaction committed
# 失效时日志缺失:
[main] DEBUG o.s.t.i.TransactionInterceptor - Getting transaction for [OrderService.create]
[main] DEBUG o.h.SQL - INSERT INTO orders ...
# ← 这里直接没了下面的 Completing 日志,说明内部调用没经过代理
正确的事务模板
@Service
public class OrderService {
@Autowired private OrderRepository orderRepo;
@Autowired private InventoryRepository invRepo;
@Autowired private EventPublisher publisher;
/**
* 创建订单 + 扣库存(同一本地事务)
* 规则:rollbackFor=Exception.class,确保所有异常都回滚
* propagation=REQUIRED(默认),加入外层事务
* timeout=5,5 秒不完成自动回滚,避免长事务锁表
*/
@Transactional(rollbackFor = Exception.class, timeout = 5)
public Order create(CreateOrderCmd cmd) {
// 1. 幂等检查
if (orderRepo.existsByIdempotencyKey(cmd.getIdempotencyKey())) {
return orderRepo.findByIdempotencyKey(cmd.getIdempotencyKey());
}
// 2. 创建订单
Order order = new Order(cmd);
orderRepo.save(order);
// 3. 扣库存(乐观锁)
int updated = invRepo.deductWithVersion(cmd.getSku(), cmd.getQty());
if (updated == 0) {
throw new BusinessException("inventory_changed");
}
// 4. 发事件(注意:这个事件要在事务提交后才发,否则可能消费者拿到了订单 id 但 DB 还没提交)
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override
public void afterCommit() {
publisher.publish(new OrderCreatedEvent(order.getId()));
}
}
);
return order;
}
}
这套模板有几个亮点:
rollbackFor = Exception.class兜底timeout = 5防长事务- 事务内幂等检查
- 用乐观锁防并发扣库存
- 事件用
afterCommit确保事务真的提交了再发
事务 review 7 问
- 方法是 public 吗?
- 是不是被同类内部调用?
- 异常有没有被吞?
- 有没有 rollbackFor = Exception.class?
- 数据源支持事务吗?(InnoDB,不是 MyISAM)
- 是不是跨数据源?
- Propagation 是不是默认 REQUIRED?(除非有特殊需求)
把这 7 问贴到 PR 模板里,Spring 事务类 bug 直接降到一年遇到 1-2 次。代价 0,收益高。
—— 别看了 · 2026