引言

前置知识

在深入问题之前,我们先简单回顾Spring事务的核心机制:

  • PlatformTransactionManager:事务管理器核心接口
  • @Transactional:声明式事务注解
  • 事务传播机制:PROPAGATION_REQUIRED、PROPAGATION_REQUIRES_NEW等
  • AOP代理机制:Spring通过代理实现事务管理

陷阱一:非Public方法

现象描述

在非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("年龄不能为负数");
        }
    }
}

解决方案

  • 将需要使用事务的方法声明为public
  • 如果需要保护方法访问,可以使用其他访问控制机制

陷阱二:自调用问题

现象描述

在同一个类中,一个方法调用另一个带有@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默认只在抛出RuntimeExceptionError时回滚事务。如果异常被捕获,或者抛出的是受检异常(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种传播行为的使用场景和陷阱》

记得关注和点赞,获取更多技术干货!

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:[email protected]