肌肉王养成记苹果版
216.6M · 2025-11-14
在深入问题之前,我们先简单回顾Spring事务的核心机制:
在非public方法上使用@Transactional注解,事务完全不生效。
Spring事务管理基于AOP代理实现。在默认配置下,Spring的AOP代理只能拦截public方法。这是由于Spring使用的代理机制(JDK动态代理和CGLIB)的限制所致。
@Service
public class UserService {
// 错误示例:事务不会生效
@Transactional
void createUserInternal(User user) {
userMapper.insert(user);
// 如果这里出现异常,事务不会回滚
if (user.getAge() < 0) {
throw new RuntimeException("年龄不能为负数");
}
}
// 正确示例:使用public修饰符
@Transactional
public void createUser(User user) {
userMapper.insert(user);
if (user.getAge() < 0) {
throw new RuntimeException("年龄不能为负数");
}
}
}
在同一个类中,一个方法调用另一个带有@Transactional注解的方法,事务不生效。
Spring通过代理对象来管理事务。当在同一个类中直接调用方法时,调用的是目标对象的方法,而不是代理对象的方法,因此事务拦截器不会起作用。
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
public void placeOrder(Order order) {
// 其他业务逻辑
validateOrder(order);
// 自调用 - 事务不会生效!
createOrder(order);
// 正确的调用方式
// orderService.createOrder(order);
}
@Transactional
public void createOrder(Order order) {
orderMapper.insert(order);
updateInventory(order.getItems());
// 如果更新库存失败,期望订单创建回滚,但自调用时不会回滚
}
private void updateInventory(List<OrderItem> items) {
for (OrderItem item : items) {
if (item.getQuantity() > getStock(item.getProductId())) {
throw new RuntimeException("库存不足");
}
// 更新库存...
}
}
}
方案一:使用AopContext获取代理对象
@Service
public class OrderService {
public void placeOrder(Order order) {
validateOrder(order);
// 通过AopContext获取代理对象
OrderService proxy = (OrderService) AopContext.currentProxy();
proxy.createOrder(order);
}
@Transactional
public void createOrder(Order order) {
// 事务操作...
}
}
需要在启动类上添加@EnableAspectJAutoProxy(exposeProxy = true)
方案二:重构代码结构
@Service
public class OrderService {
@Autowired
private OrderTransactionService orderTransactionService;
public void placeOrder(Order order) {
validateOrder(order);
orderTransactionService.createOrder(order);
}
}
@Service
public class OrderTransactionService {
@Transactional
public void createOrder(Order order) {
// 事务操作...
}
}
方法中抛出了异常,但事务没有回滚,因为异常在方法内部被捕获处理了。
Spring默认只在抛出RuntimeException和Error时回滚事务。如果异常被捕获,或者抛出的是受检异常(Checked Exception),事务不会自动回滚。
@Service
public class PaymentService {
@Autowired
private PaymentMapper paymentMapper;
@Autowired
private AccountService accountService;
// 错误示例:异常被捕获
@Transactional
public void processPayment(Payment payment) {
try {
paymentMapper.insert(payment);
accountService.deductBalance(payment.getUserId(), payment.getAmount());
// 可能抛出受检异常
sendPaymentNotification(payment);
} catch (Exception e) {
// 异常被捕获,事务不会回滚!
log.error("支付处理失败", e);
}
}
// 错误示例:抛出受检异常
@Transactional
public void processPaymentWithCheckedException(Payment payment) throws IOException {
paymentMapper.insert(payment);
accountService.deductBalance(payment.getUserId(), payment.getAmount());
if (payment.getAmount().compareTo(BigDecimal.ZERO) < 0) {
// 受检异常,默认不会触发回滚
throw new IOException("支付金额不能为负数");
}
}
}
方案一:手动回滚
@Transactional
public void processPayment(Payment payment) {
try {
paymentMapper.insert(payment);
accountService.deductBalance(payment.getUserId(), payment.getAmount());
sendPaymentNotification(payment);
} catch (Exception e) {
log.error("支付处理失败", e);
// 手动设置回滚
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
throw new RuntimeException("支付失败", e);
}
}
方案二:指定回滚异常
// 指定所有Exception都触发回滚
@Transactional(rollbackFor = Exception.class)
public void processPaymentWithCheckedException(Payment payment) throws IOException {
paymentMapper.insert(payment);
accountService.deductBalance(payment.getUserId(), payment.getAmount());
if (payment.getAmount().compareTo(BigDecimal.ZERO) < 0) {
throw new IOException("支付金额不能为负数");
}
}
// 更精确的控制
@Transactional(rollbackFor = {BusinessException.class, IOException.class},
noRollbackFor = {ValidationException.class})
public void processPaymentWithSpecificExceptions(Payment payment) {
// 业务逻辑...
}
``
## 陷阱四:数据库引擎不支持事务
### 现象描述
在MySQL中使用MyISAM存储引擎,事务注解完全无效。
### 原理分析
MyISAM是MySQL的默认存储引擎(在旧版本中),但它**不支持事务**。只有支持事务的存储引擎(如InnoDB)才能正常工作。
### 解决方案
**检查并修改数据库引擎:**
```sql
-- 检查表使用的存储引擎
SHOW TABLE STATUS LIKE 'your_table_name';
-- 修改表存储引擎为InnoDB
ALTER TABLE your_table_name ENGINE=InnoDB;
-- 创建新表时指定存储引擎
CREATE TABLE example (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100)
) ENGINE=InnoDB;
Spring配置中验证事务管理器:
@Configuration
@EnableTransactionManagement
public class TransactionConfig {
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
期望一个操作在独立事务中执行,但由于传播机制设置不当,导致事务行为不符合预期。
Spring提供了多种事务传播机制,错误的使用会导致事务边界混乱。
@Service
public class AuditService {
@Autowired
private AuditMapper auditMapper;
// 错误使用传播机制
@Transactional(propagation = Propagation.SUPPORTS)
public void logOperation(String operation) {
auditMapper.insert(new AuditLog(operation));
// 如果当前没有事务,这个插入操作不会在事务中执行
}
}
@Service
public class BusinessService {
@Autowired
private AuditService auditService;
@Transactional
public void businessMethod() {
// 主业务逻辑...
// 审计日志 - 期望独立事务,但实际使用了主事务
auditService.logOperation("业务操作完成");
}
}
@Service
public class AuditService {
// 正确使用REQUIRES_NEW
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logOperationInNewTransaction(String operation) {
auditMapper.insert(new AuditLog(operation));
// 这个操作会在独立的新事务中执行
// 即使外部事务回滚,审计日志仍然会保存
}
// 正确使用NOT_SUPPORTED
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void logOperationWithoutTransaction(String operation) {
auditMapper.insert(new AuditLog(operation));
// 这个操作会在无事务环境中执行
}
}
在方法中手动获取Connection并执行commit,干扰了Spring的事务管理。
Spring通过ThreadLocal来管理事务上下文,手动操作Connection会破坏这种管理机制。
// 错误示例:手动管理Connection
@Transactional
public void updateWithManualCommit(Data data) {
Connection conn = null;
try {
conn = dataSource.getConnection();
conn.setAutoCommit(false);
// 执行SQL操作
updateData(conn, data);
// 手动提交
conn.commit();
} catch (SQLException e) {
if (conn != null) {
conn.rollback();
}
throw new RuntimeException(e);
} finally {
if (conn != null) {
conn.close();
}
}
}
完全依赖Spring的事务管理:
@Transactional
public void updateWithSpringTransaction(Data data) {
// 直接使用Spring管理的数据访问组件
jdbcTemplate.update("UPDATE table SET column = ? WHERE id = ?",
data.getValue(), data.getId());
// Spring会自动处理事务提交和回滚
}
在异步方法中使用@Transactional,事务上下文无法传递到新线程。
事务信息存储在ThreadLocal中,异步执行时会切换到新的线程,事务上下文丢失。
@Service
public class AsyncService {
@Async
@Transactional
public void asyncProcess(Data data) {
// 这个事务不会生效!
dataMapper.insert(data);
processData(data);
}
}
方案一:在调用异步方法前完成事务操作
@Service
public class MainService {
@Autowired
private AsyncService asyncService;
@Transactional
public void mainProcess(Data data) {
// 在主事务中完成核心数据操作
dataMapper.insert(data);
// 异步处理非核心业务
asyncService.asyncProcessNonCritical(data);
}
}
@Service
public class AsyncService {
@Async
public void asyncProcessNonCritical(Data data) {
// 非核心的异步处理,不要求事务
sendNotification(data);
updateCache(data);
}
}
方案二:使用编程式事务管理
@Service
public class AsyncService {
@Autowired
private TransactionTemplate transactionTemplate;
@Async
public void asyncProcessWithTransaction(Data data) {
transactionTemplate.execute(status -> {
// 在编程式事务中执行
dataMapper.insert(data);
processData(data);
return null;
});
}
}
在多数据源环境下,事务管理器配置不正确,导致事务无法正确绑定到对应的数据源。
当存在多个数据源时,需要明确指定每个事务使用哪个事务管理器。
// 错误配置:没有指定事务管理器
@Configuration
@EnableTransactionManagement
public class MultiDataSourceConfig {
@Bean
@Primary
public DataSource primaryDataSource() {
// 主数据源配置
}
@Bean
public DataSource secondaryDataSource() {
// 次要数据源配置
}
// 只配置了一个事务管理器
@Bean
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(primaryDataSource());
}
}
正确配置多数据源事务管理:
@Configuration
@EnableTransactionManagement
public class MultiDataSourceConfig {
@Bean
@Primary
public DataSource primaryDataSource() {
// 主数据源配置
}
@Bean
public DataSource secondaryDataSource() {
// 次要数据源配置
}
@Bean
@Primary
public PlatformTransactionManager primaryTransactionManager() {
return new DataSourceTransactionManager(primaryDataSource());
}
@Bean
public PlatformTransactionManager secondaryTransactionManager() {
return new DataSourceTransactionManager(secondaryDataSource());
}
}
// 使用指定的事务管理器
@Service
public class UserService {
@Transactional(transactionManager = "primaryTransactionManager")
public void primaryDatabaseOperation(User user) {
// 使用主数据源的事务
}
@Transactional(transactionManager = "secondaryTransactionManager")
public void secondaryDatabaseOperation(Log log) {
// 使用次要数据源的事务
}
}
| 陷阱场景 | 问题现象 | 解决方案 |
|---|---|---|
| 非Public方法 | 事务完全不生效 | 将方法改为public |
| 自调用问题 | 同类调用事务失效 | 使用AopContext或重构代码结构 |
| 异常被捕获 | 异常处理但事务未回滚 | 手动回滚或指定rollbackFor |
| 数据库引擎不支持 | 事务注解无效 | 使用InnoDB等支持事务的引擎 |
| 错误的传播机制 | 事务边界混乱 | 根据业务需求选择合适的传播机制 |
| 方法内手动提交 | 干扰Spring事务管理 | 完全依赖Spring声明式事务 |
| 异步方法调用 | 事务上下文丢失 | 分离事务操作与异步处理 |
| 多数据源配置错误 | 事务绑定错误数据源 | 明确配置和指定事务管理器 |
查看事务状态:
@Transactional
public void debugTransaction() {
TransactionStatus status = TransactionAspectSupport.currentTransactionStatus();
System.out.println("是否是新事务: " + status.isNewTransaction());
System.out.println("是否有保存点: " + status.hasSavepoint());
System.out.println("是否已完成: " + status.isCompleted());
}
启用事务调试日志:
# application.properties
logging.level.org.springframework.transaction.interceptor=TRACE
logging.level.org.springframework.jdbc.datasource.DataSourceTransactionManager=DEBUG
你在项目中还遇到过哪些Spring事务相关的问题?是否有其他有趣的"坑"想要分享?欢迎在评论区留言讨论!
下一篇预告:《Spring事务传播机制深度解析:7种传播行为的使用场景和陷阱》
记得关注和点赞,获取更多技术干货!