事务失效场景大全 - private 方法、自调用、try-catch 吞异常等
一、概述
Spring 声明式事务(@Transactional)极大地简化了企业级应用的事务管理,但其底层基于 AOP 代理 实现,存在若干天然限制。若不了解这些限制,很容易在不知不觉中写出"事务失效"的代码——方法执行期间发生了异常,数据却没有被回滚,导致数据不一致甚至生产事故。
本文系统性梳理 Spring 事务失效的 8 种典型场景,深入分析每种场景的根本原因,并提供可执行的解决方案与生产级案例。
二、8 大事务失效场景
场景 1:private/protected 方法上使用 @Transactional
问题描述
在 private 或 protected 方法上标注 @Transactional,事务不生效。
错误代码示例
@Service
public class UserService {
@Transactional
private void saveUser(User user) {
userDao.insert(user);
// 此处抛出 RuntimeException,但不会被回滚
throw new RuntimeException("模拟异常");
}
public void register(User user) {
saveUser(user); // 事务不生效
}
}根本原因分析
Spring 声明式事务基于 AOP 代理实现。无论是 JDK 动态代理还是 CGLIB 代理,都只拦截 public 方法。
- JDK 动态代理:基于接口代理,只能代理接口中的 public 方法。
- CGLIB 代理:通过生成子类字节码实现代理,但 Spring 默认只对 public 方法应用事务拦截器(
AbstractFallbackTransactionAttributeSource中仅查找 public 方法的事务属性)。
查看 Spring 源码 AbstractFallbackTransactionAttributeSource.computeTransactionAttribute():
// 只有 public 方法才会被赋予事务属性
if (method instanceof Method) {
if (!Modifier.isPublic(method.getModifiers())) {
return null; // 非 public 方法,直接返回 null → 事务失效
}
}解决方案
将 private 改为 public,或将事务逻辑提取到独立 Service Bean 中。
@Service
public class UserService {
@Transactional
public void saveUser(User user) {
userDao.insert(user);
throw new RuntimeException("模拟异常"); // 现在可以正常回滚
}
public void register(User user) {
saveUser(user);
}
}场景 2:自调用——同一类中的方法调用不触发 AOP 代理
问题描述
在同一个 Service 类中,方法 A(非事务)调用方法 B(带 @Transactional),方法 B 的事务不生效。
错误代码示例
@Service
public class OrderService {
public void createOrder(Order order) {
// 自调用——不会触发 AOP 代理
this.saveOrder(order);
}
@Transactional
public void saveOrder(Order order) {
orderDao.insert(order);
detailDao.insert(order.getDetail());
// 如果这里抛出异常,saveOrder 的事务不会回滚
throw new RuntimeException("库存不足");
}
}根本原因分析
Spring AOP 代理的工作机制是:调用代理对象的方法时,会先经过拦截器链,再执行目标方法。但当类内部使用 this.method() 调用时,this 指向的是原始目标对象,而非代理对象,因此 AOP 拦截器不会被触发,@Transactional 注解被直接忽略。
外部调用 → 代理对象 → 拦截器(开启事务)→ 目标方法
自调用 → 原始对象 → 目标方法(无拦截器,事务失效)解决方案
方案一:注入自身代理(推荐)
@Service
public class OrderService {
@Autowired
private OrderService self; // 注入自身代理
public void createOrder(Order order) {
self.saveOrder(order); // 通过代理对象调用
}
@Transactional
public void saveOrder(Order order) {
orderDao.insert(order);
detailDao.insert(order.getDetail());
throw new RuntimeException("库存不足"); // 正常回滚
}
}方案二:使用 AopContext.currentProxy()
@Service
@EnableAspectJAutoProxy(exposeProxy = true) // 需要暴露代理对象
public class OrderService {
public void createOrder(Order order) {
((OrderService) AopContext.currentProxy()).saveOrder(order);
}
@Transactional
public void saveOrder(Order order) {
// ...
}
}方案三:拆分到不同的 Service 类
@Service
public class OrderService {
@Autowired
private OrderPersistenceService persistenceService;
public void createOrder(Order order) {
persistenceService.saveOrder(order); // 跨类调用 → AOP 生效
}
}
@Service
public class OrderPersistenceService {
@Transactional
public void saveOrder(Order order) {
// ...
}
}场景 3:try-catch 吞掉异常
问题描述
在 @Transactional 方法内部使用 try-catch 捕获了异常但未重新抛出,事务管理器感知不到异常发生,触发事务提交而非回滚。
错误代码示例
@Service
public class PaymentService {
@Transactional
public void transfer(TransferDTO dto) {
try {
accountDao.debit(dto.getFrom(), dto.getAmount()); // 扣款成功
accountDao.credit(dto.getTo(), dto.getAmount()); // 入账失败抛出异常
} catch (Exception e) {
log.error("转账失败", e);
// 异常被捕获未重新抛出 → 事务管理器认为方法正常结束 → 提交事务
// 结果:扣款已执行,入账未执行,资金丢失
}
}
}根本原因分析
Spring 事务回滚机制依赖于运行时异常(RuntimeException 或 Error)从 @Transactional 方法中传播出来。TransactionAspectSupport 的 invokeWithinTransaction() 方法捕获到异常后调用 rollbackOn() 判断是否回滚。
如果异常被 catch 代码块拦截且未重新抛出,事务拦截器认为方法已正常返回,于是执行 commitTransactionAfterReturning(),将当前事务提交。
异常与回滚逻辑对照
| 情况 | 结果 |
|---|---|
| 未捕获异常,抛出 RuntimeExcpetion | 回滚 |
| 捕获异常,未重新抛出 | 提交(失效) |
| 捕获异常,重新抛出 RuntimeException | 回滚 |
| 捕获异常,重新抛出 Checked Exception(默认) | 提交(失效) |
解决方案
方案一:捕获异常后重新抛出(推荐)
@Transactional
public void transfer(TransferDTO dto) {
try {
accountDao.debit(dto.getFrom(), dto.getAmount());
accountDao.credit(dto.getTo(), dto.getAmount());
} catch (Exception e) {
log.error("转账失败,即将回滚事务", e);
throw new RuntimeException("转账失败,事务回滚", e); // 重新抛出
}
}方案二:在 catch 块中手动回滚
@Transactional
public void transfer(TransferDTO dto) {
try {
accountDao.debit(dto.getFrom(), dto.getAmount());
accountDao.credit(dto.getTo(), dto.getAmount());
} catch (Exception e) {
log.error("转账失败", e);
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}方案三:配置回滚规则,支持 Checked Exception
@Transactional(rollbackFor = Exception.class) // 默认只回滚 RuntimeException
public void transfer(TransferDTO dto) throws BusinessException {
try {
accountDao.debit(dto.getFrom(), dto.getAmount());
accountDao.credit(dto.getTo(), dto.getAmount());
} catch (Exception e) {
log.error("转账失败", e);
throw new BusinessException("转账失败", e); // Checked Exception,配置 rollbackFor 后生效
}
}场景 4:传播属性配置错误
问题描述
事务传播行为配置不当,导致预期应开启新事务的方法实际在已有事务中运行,异常回滚时影响范围超出预期。
典型错误:REQUIRES_NEW 未生效
@Service
public class OrderService {
@Autowired
private LogService logService;
@Transactional
public void placeOrder(Order order) {
orderDao.insert(order);
logService.recordLog("创建订单: " + order.getId());
// placeOrder 事务异常,日志也跟着回滚了
throw new RuntimeException("订单创建失败");
}
}
@Service
public class LogService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void recordLog(String message) {
logDao.insert(new Log(message));
}
}根本原因分析
REQUIRES_NEW 的语义是:如果当前存在事务,则挂起当前事务,开启一个新事务。但在自调用或代理未正确配置的情况下(如场景 2),外部方法的事务已开启,内部 REQUIRES_NEW 方法在同一事务上下文中执行,导致日志回滚。
此外,REQUIRES_NEW 依赖底层事务资源(如 JDBC Connection)的挂起/恢复机制,若事务管理器实现不完整或数据源不支持保存点,则可能静默降级为使用已有事务。
解决方案
确保 REQUIRES_NEW 方法通过代理对象调用,且事务管理器行为正确:
@Service
public class OrderService {
@Autowired
private LogService logService; // 注入独立 Bean
@Transactional
public void placeOrder(Order order) {
orderDao.insert(order);
logService.recordLog("创建订单: " + order.getId()); // 跨 Bean 调用,AOP 生效
throw new RuntimeException("订单创建失败");
}
}
@Service
public class LogService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void recordLog(String message) {
logDao.insert(new Log(message));
// 即使外部事务回滚,此处的日志提交不受影响
}
}常见传播属性速查表:
| 传播属性 | 当前无事务 | 当前有事务 | 典型场景 |
|---|---|---|---|
| REQUIRED(默认) | 新建事务 | 加入当前事务 | 通用业务操作 |
| REQUIRES_NEW | 新建事务 | 挂起当前,新建事务 | 操作日志、审计 |
| NESTED | 新建事务 | 嵌套事务(JDBC savepoint) | 批量处理,部分回滚 |
| MANDATORY | 抛出异常 | 加入当前事务 | 强制在事务中执行 |
| NEVER | 正常执行 | 抛出异常 | 禁止在事务中执行 |
| SUPPORTS | 不开启事务 | 加入当前事务 | 查询方法 |
| NOT_SUPPORTED | 不开启事务 | 挂起当前事务 | 内部异步回调 |
场景 5:多数据源事务——DataSource 切换失效
问题描述
应用配置了多个数据源(主库 + 从库),@Transactional 注解实际管理的是默认数据源的事务,对另一个数据源的写操作不生效。
错误代码示例
@Service
public class ReportService {
@Autowired
@Qualifier("primaryPlatformTransactionManager")
private PlatformTransactionManager primaryTransactionManager;
@Transactional // 使用默认事务管理器(主库)
public void syncReport(Report report) {
// 1. 写入主库
primaryReportDao.insert(report);
// 2. 写入从库(假设 reportLogDao 使用 secondaryDataSource)
reportLogDao.insert(report.getLog());
// 从库操作不在主库事务管理范围内
// 若从库写入失败,主库事务已提交 → 数据不一致
}
}根本原因分析
Spring 事务管理器与 DataSource 一一绑定。@Transactional 不指定 transactionManager 时,使用默认的 PlatformTransactionManager Bean,该事务管理器只管理与之关联的 DataSource。对其他数据源的 JDBC 操作不受该事务控制。
解决方案
方案一:指定 transactionManager
@Service
public class ReportService {
@Transactional("primaryTransactionManager")
public void writePrimary(Report report) {
primaryReportDao.insert(report);
}
@Transactional("secondaryTransactionManager")
public void writeSecondary(ReportLog log) {
reportLogDao.insert(log);
}
}方案二:使用分布式事务(JTA / Seata)
当需要强一致性时,引入分布式事务框架:
// 使用 JTA 事务管理器(如 Atomikos)
@Bean
public PlatformTransactionManager transactionManager() {
return new JtaTransactionManager();
}
// 或使用 Seata @GlobalTransactional
@GlobalTransactional
public void syncReport(Report report) {
primaryReportDao.insert(report);
reportLogDao.insert(report.getLog());
}方案三:编程式事务 + TransactionTemplate
@Service
public class ReportService {
@Autowired
@Qualifier("primaryTransactionTemplate")
private TransactionTemplate primaryTemplate;
@Autowired
@Qualifier("secondaryTransactionTemplate")
private TransactionTemplate secondaryTemplate;
public void syncReport(Report report) {
// 各自提交/回滚,最终一致性方案
primaryTemplate.execute(status -> {
primaryReportDao.insert(report);
return null;
});
secondaryTemplate.execute(status -> {
reportLogDao.insert(report.getLog());
return null;
});
}
}场景 6:@Transactional 注解在非 public 方法上
注意:此场景与场景 1 有重叠,但有独立的关注点——即便 Spring 版本允许非 public 方法的事务配置,仍存在兼容性和代理机制层面的问题。
问题描述
@Transactional 标注在 protected、package-private(默认权限)方法上,Spring 不会为此类方法创建事务。
错误代码示例
@Service
public class InventoryService {
@Transactional
protected void deductStock(Long productId, int quantity) {
inventoryDao.deduct(productId, quantity);
}
@Transactional
void updateInventory(InventoryDTO dto) {
inventoryDao.update(dto);
}
public void processOrder(Order order) {
deductStock(order.getProductId(), order.getQuantity()); // 事务失效
updateInventory(order.getInventory()); // 事务失效
}
}根本原因分析
Spring 的 AbstractFallbackTransactionAttributeSource 在 computeTransactionAttribute() 方法中明确对非 public 方法返回 null:
@Override
protected TransactionAttribute computeTransactionAttribute(
Method method, Class<?> targetClass) {
// Don't allow non-public methods, as configured.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
// ...
}Spring 6.x / Boot 3.x 中 allowPublicMethodsOnly() 默认返回 true。
解决方案
将方法访问修饰符改为 public。
@Service
public class InventoryService {
@Transactional
public void deductStock(Long productId, int quantity) {
inventoryDao.deduct(productId, quantity);
}
@Transactional
public void updateInventory(InventoryDTO dto) {
inventoryDao.update(dto);
}
public void processOrder(Order order) {
deductStock(order.getProductId(), order.getQuantity()); // 通过代理调用
updateInventory(order.getInventory());
}
}场景 7:数据库引擎不支持事务
问题描述
@Transactional 配置正确、代码逻辑无误、无任何异常被吞掉,但数据依旧不回滚。
错误代码示例
// application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb
driver-class-name: com.mysql.cj.jdbc.Driver
// 业务代码
@Service
public class ProductService {
@Transactional
public void batchImport(List<Product> products) {
for (Product p : products) {
productDao.insert(p);
}
throw new RuntimeException("导入失败,应回滚");
}
}
// 实际效果:数据已插入,未回滚根本原因分析
MySQL 默认使用 InnoDB 引擎支持事务,但若表被创建为 MyISAM 引擎,则根本不支持事务。MyISAM 的 DML 操作是自动提交的,每个 INSERT / UPDATE / DELETE 立即持久化,无法回滚。
-- 检查表的存储引擎
SHOW TABLE STATUS WHERE Name = 'product';
-- Engine: MyISAM → 不支持事务解决方案
步骤一:将表引擎改为 InnoDB
ALTER TABLE product ENGINE = InnoDB;步骤二:修改建表 DDL,默认使用 InnoDB
CREATE TABLE product (
id BIGINT PRIMARY KEY,
name VARCHAR(255)
) ENGINE = InnoDB;步骤三:配置 MySQL 默认引擎为 InnoDB
# my.cnf
default-storage-engine=InnoDB步骤四(可选):配置方言进行校验
spring:
jpa:
database-platform: org.hibernate.dialect.MySQL8Dialect
jpa:
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL8Dialect场景 8:事务管理器未正确配置
问题描述
@Transactional 注解添加后无任何效果,事务像完全不存在一样——异常抛出后数据照常写入。
错误代码示例
// Spring Boot 项目,依赖于 spring-boot-starter-jdbc
// application.yml 中配置了数据源,但未配置事务管理器
@Service
public class ConfigService {
@Transactional
public void saveConfig(Config config) {
configDao.insert(config);
throw new RuntimeException("测试回滚");
}
}// 或者自定义配置中遗漏了关键组件
@Configuration
public class AppConfig {
// 缺少以下两个 Bean:
// 1. PlatformTransactionManager
// 2. @EnableTransactionManagement
}根本原因分析
Spring 声明式事务需要两个基础设施:
PlatformTransactionManagerBean:提供事务的开启、提交、回滚能力。@EnableTransactionManagement:激活@Transactional注解的解析与拦截器注册(Spring Boot 自动配置下自动开启,但自定义配置中容易遗漏)。
Spring Boot 在引入 spring-boot-starter-jdbc 或 spring-boot-starter-data-jpa 时,会自动配置 DataSourceTransactionManager 或 JpaTransactionManager。但以下情况会导致自动配置失效:
- 自定义了
DataSourceBean 但未同时提供PlatformTransactionManager。 - 项目中存在多个
DataSource但未指定主事务管理器。 - 手动排除了
DataSourceTransactionManagerAutoConfiguration。
解决方案
方案一:确保自动配置正常(推荐)
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() {
// 确保 spring-boot-starter-jdbc 在 classpath 中
return DataSourceBuilder.create()
.url("jdbc:mysql://localhost:3306/db")
.username("root")
.password("password")
.build();
}
// Spring Boot 会自动创建 DataSourceTransactionManager,
// 无需手动声明。但如果自定义 DataSource,有时需要显式声明:
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}方案二:手动启用注解驱动
@Configuration
@EnableTransactionManagement // 显式开启注解事务支持
public class AppConfig {
@Bean
public DataSource dataSource() {
// ...
}
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}方案三:多事务管理器时指定默认值
@Configuration
public class TransactionConfig {
@Primary
@Bean
public PlatformTransactionManager primaryTransactionManager(
@Qualifier("primaryDataSource") DataSource ds) {
return new DataSourceTransactionManager(ds);
}
@Bean
public PlatformTransactionManager secondaryTransactionManager(
@Qualifier("secondaryDataSource") DataSource ds) {
return new DataSourceTransactionManager(ds);
}
}三、实战案例:支付系统事务失效导致重复扣款
事故背景
某电商支付服务在一次大促期间出现重复扣款问题:用户下单后支付接口返回超时,前端提示"支付失败",但用户的银行账户实际已被扣款。总计影响订单 327 笔,涉及金额 ¥48,952.00。
故障代码还原
@Service
public class PaymentService {
@Transactional
public void processPayment(PaymentRequest request) {
// 1. 调用第三方支付网关
PaymentResponse response = paymentGateway.charge(request);
// 2. 更新订单状态
orderService.updateStatus(request.getOrderId(), OrderStatus.PAID);
// 3. 记录支付流水
paymentRecordDao.insert(new PaymentRecord(request, response));
}
}
@Service
public class OrderService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateStatus(Long orderId, OrderStatus status) {
orderDao.updateStatus(orderId, status);
}
}问题分析(层层递进)
第一层:支付网关返回超时异常
当 paymentGateway.charge() 调用第三方接口超时时,抛出 GatewayTimeoutException(继承自 Exception),而 @Transactional 默认只回滚 RuntimeException,因此事务未回滚——扣款虽然失败,但后续的订单状态更新和支付流水记录却已持久化。
第二层:重试机制放大了问题
前端收到超时后自动发起重试,系统接到重试请求后再次调用 paymentGateway.charge()——这次成功了,于是生成了第二笔支付流水。实际银行侧产生了两次扣款。
第三层:自调用导致 REQUIRES_NEW 失效
orderService.updateStatus() 方法标注了 REQUIRES_NEW,本意是让订单状态更新独立于主事务提交。但由于 orderService 是独立 Bean,跨 Bean 调用时 AOP 正常生效。问题在于 PaymentService 本身的事务已经包含了状态更新——即便 updateStatus 开启了新事务,主事务回滚时 PaymentRecord 仍被写入。
完整的故障链路
请求 1 进入 PaymentService.processPayment()
└─ @Transactional 开启事务 TX1
├─ paymentGateway.charge() → 超时(GatewayTimeoutException,Checked Exception)
│ → 事务拦截器捕获异常,**判断不回滚**(非 RuntimeException)
│ → 异常继续向上抛出
├─ orderService.updateStatus() → REQUIRES_NEW → 开启 TX2 → 提交
│ → 订单状态标记为 "已支付"
└─ paymentRecordDao.insert() → 写入 TX1 → TX1 提交(因为异常未被识别为回滚信号)
└─ 返回 GatewayTimeoutException
请求 2 进入 PaymentService.processPayment()(重试)
└─ @Transactional 开启事务 TX3
├─ paymentGateway.charge() → 成功
├─ orderService.updateStatus() → 订单状态再次标记 "已支付"
└─ paymentRecordDao.insert() → 写入第二条流水
结果:扣款 2 次,订单被支付 1 次(状态为已支付),流水多出 1 条修复方案
@Service
public class PaymentService {
@Transactional(rollbackFor = Exception.class) // 关键:回滚所有异常
public void processPayment(PaymentRequest request) {
try {
PaymentResponse response = paymentGateway.charge(request);
paymentRecordDao.insert(new PaymentRecord(request, response));
orderService.updateStatus(request.getOrderId(), OrderStatus.PAID);
} catch (GatewayTimeoutException e) {
// 超时异常:记录日志,事务回滚,保证数据一致性
log.warn("支付网关超时,事务回滚: orderId={}", request.getOrderId());
throw new PaymentException("支付网关超时,请重试", e); // 重新抛出触发回滚
}
}
}
@Service
public class OrderService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateStatus(Long orderId, OrderStatus status) {
orderDao.updateStatus(orderId, status);
}
}此外,还需要增加以下配套措施:
- 幂等性校验:支付接口增加幂等键(idempotent key),相同请求只处理一次。
- 重试策略优化:重试前先查询支付状态,避免重复扣款。
- 对账机制:每日与银行侧对账,发现差异及时冲正。
- 事务日志:在事务边界打印日志,方便排查。
四、排查清单(Checklist)
以下是从代码审查角度出发的 Spring 事务失效排查清单,可按顺序逐项检查。
4.1 注解配置检查
| # | 检查项 | 检查方法 | 预期结果 |
|---|---|---|---|
| 1 | @Transactional 是否标注在 public 方法上 | 检查方法访问修饰符 | public |
| 2 | @Transactional 是否标注在接口而非实现类上(JDK 代理时) | 查看注解位置 | 标注在实现类或接口方法上 |
| 3 | @EnableTransactionManagement 是否开启(非 Boot 项目) | 搜索 @EnableTransactionManagement | 存在 |
| 4 | 注解是否被正确解析(检查是否使用了代理/类代理模式) | 查看 @EnableTransactionManagement(mode=) | ASPECTJ 或 PROXY |
4.2 代理机制检查
| # | 检查项 | 检查方法 | 预期结果 |
|---|---|---|---|
| 5 | 是否存在类内部自调用(this.method()) | 搜索 this. 调用的事务方法 | 无自调用 |
| 6 | 是否注入了自身代理(@Autowired self) | 检查自调用修复方式 | 正确使用代理或拆分 |
| 7 | AOP 代理是否创建成功 | 启动日志中搜索 Creating implicit proxy | 有对应 Bean 的代理创建记录 |
| 8 | @Transactional 类是否被 AOP 切面拦截 | 调试时检查对象类型 | 出现 EnhancerBySpringCGLIB 或 $Proxy |
4.3 异常处理检查
| # | 检查项 | 检查方法 | 预期结果 |
|---|---|---|---|
| 9 | 事务方法内异常是否被 try-catch 吞掉 | 搜索 catch 块中是否有 throw | 异常被重新抛出或手动 setRollbackOnly() |
| 10 | rollbackFor 是否覆盖了方法抛出的所有异常类型 | 检查 @Transactional(rollbackFor=...) | Checked Exception 已被包含 |
| 11 | 方法签名上声明的 throws 异常是否被正确处理 | 对比异常类型和 rollbackFor | Checked Exception 有对应回滚配置 |
| 12 | 异常是否被 Filter/Interceptor 提前捕获 | 检查 Filter 链中是否有全局异常处理 | 异常未被提前消费 |
4.4 事务传播行为检查
| # | 检查项 | 检查方法 | 预期结果 |
|---|---|---|---|
| 13 | REQUIRES_NEW / NESTED 是否通过代理对象调用 | 检查调用链 | 跨 Bean 调用或自注入 |
| 14 | 传播属性是否与业务语义匹配 | 审查每个 propagation 属性 | 符合预期行为 |
| 15 | 嵌套事务的数据库是否支持 Savepoint | 检查数据库类型 | MySQL InnoDB / PostgreSQL / Oracle |
4.5 数据源与事务管理器检查
| # | 检查项 | 检查方法 | 预期结果 |
|---|---|---|---|
| 16 | PlatformTransactionManager Bean 是否存在 | 搜索 PlatformTransactionManager 的定义 | 存在且正确配置 |
| 17 | 多数据源时 @Transactional 是否指定了正确的事务管理器 | 检查 value 或 transactionManager 属性 | 对应正确的 TM Bean 名称 |
| 18 | 数据库引擎是否支持事务 | SHOW TABLE STATUS 查看 Engine 字段 | InnoDB(MySQL) |
| 19 | 数据源的 auto-commit 是否设置为 false | 检查连接池配置 | false(Spring 事务管理器会管理) |
4.6 运行时验证
| # | 检查项 | 检查方法 | 预期结果 |
|---|---|---|---|
| 20 | 开启 DEBUG 日志级别 | 设置 logging.level.org.springframework.transaction=DEBUG | 可见 Getting transaction / Completing transaction 等日志 |
| 21 | 通过断言验证事务回滚 | 单元测试中 @Transactional + @Rollback | 测试数据自动清理 |
| 22 | 检查事务管理器是否为预期实现类 | 断点查看 TransactionInterceptor.currentTransactionStatus() | DataSourceTransactionManager / JpaTransactionManager 等 |
| 23 | 代理对象是否注入了正确的拦截器 | 检查 Advised 接口的 getAdvisors() | 包含 TransactionInterceptor |
4.7 常见配置速查
# application.yml - 事务调试配置
logging:
level:
org.springframework.transaction: DEBUG
org.springframework.jdbc.datasource: DEBUG
org.springframework.orm.jpa: DEBUG// 编程式验证事务代理
@Service
public class DebugService implements InitializingBean {
@Autowired
private ApplicationContext context;
@Override
public void afterPropertiesSet() {
Object proxy = context.getBean("debugService");
System.out.println("Proxy class: " + proxy.getClass().getName());
System.out.println("Is proxy: " + (proxy instanceof Advised));
// 预期输出类似:
// Proxy class: com.example.DebugService$$EnhancerBySpringCGLIB$$xxxx
// Is proxy: true
}
}五、总结
Spring 事务失效的根源可以归结为以下几类:
| 类别 | 典型场景 | 核心解决方案 |
|---|---|---|
| 代理限制 | private 方法、自调用 | 使用 public、自注入代理、拆分 Bean |
| 异常被吞 | try-catch 不重新抛出 | 重新抛出 RuntimeException、配置 rollbackFor |
| 传播配置错误 | REQUIRES_NEW 在自调用中失效 | 跨 Bean 调用、使用独立事务管理器 |
| 基础设施缺失 | 无事务管理器、MyISAM 引擎 | 正确配置 PlatformTransactionManager 和数据库 |
| 多数据源 | 跨数据源操作不统一提交 | 指定 transactionManager、引入分布式事务 |
最佳实践建议:
- 默认使用
@Transactional(rollbackFor = Exception.class),主动涵盖 Checked Exception。 - 事务方法一律定义为
public,避免代理限制。 - 避免类内部自调用,通过注入自身代理或拆分 Service 解决。
- 在 catch 块中打印日志后重新抛出异常,不要吞掉。
- 开启事务 DEBUG 日志,开发阶段及时发现事务未生效问题。
- 编写事务回滚的单元测试,验证
@Transactional是否按预期工作。 - 生产环境建议开启慢查询和事务监控,及时发现异常事务行为。