AI摘要

文章剖析@Transactional失效的8大场景:自调用、异常被吃、非public、受检异常、异步/多线程、多数据源未指定管理器、传播行为误配、存储引擎不支持,并给出排查日志、事务同步器调试及单一职责、只读事务、异常规范等最佳实践,强调理解代理与传播机制才是根本。
记得在一次电商促销活动中,我们的订单系统出现了严重的数据不一致:用户扣款成功但订单状态未更新。查看代码,所有方法都加了@Transactional注解,理论上应该要么全部成功要么全部回滚。这次事故让我深入研究了@Transactional的工作原理,也让我明白了为什么简单的注解背后隐藏着如此多的陷阱。

一、从一个生产环境的数据不一致问题说起

先来看看当时有问题的代码:

@Service
public class OrderService {
    
    @Autowired
    private UserMapper userMapper;
    
    @Autowired  
    private OrderMapper orderMapper;
    
    @Transactional
    public void createOrder(OrderDTO orderDTO) {
        // 扣减用户余额
        userMapper.deductBalance(orderDTO.getUserId(), orderDTO.getAmount());
        
        // 创建订单记录
        Order order = convertToOrder(orderDTO);
        orderMapper.insert(order);
        
        // 发送订单创建事件
        eventPublisher.publishEvent(new OrderCreatedEvent(order.getId()));
    }
}

@Component
public class OrderEventListener {
    
    @Autowired
    private InventoryService inventoryService;
    
    @EventListener
    @Transactional  // 这里的事务注解无效!
    public void handleOrderCreated(OrderCreatedEvent event) {
        // 扣减库存
        inventoryService.deductInventory(event.getOrderId());
    }
}

在高压下,偶尔会出现用户余额已扣减,但库存扣减失败,导致数据不一致。问题的根源在于对@Transactional工作机制的理解不足。

二、@Transactional的基本工作原理

在深入失效场景前,先理解Spring事务的基本机制:

2.1 Spring事务的代理机制

Spring通过AOP代理实现事务管理:

// 简化的事务代理逻辑
public class TransactionProxy implements MethodInterceptor {
    
    private PlatformTransactionManager transactionManager;
    private Object target; // 原始业务对象
    
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        TransactionStatus status = null;
        try {
            // 1. 开启事务
            status = transactionManager.getTransaction(
                new DefaultTransactionDefinition());
            
            // 2. 执行原始方法
            Object result = invocation.proceed();
            
            // 3. 提交事务
            transactionManager.commit(status);
            return result;
        } catch (Exception e) {
            // 4. 回滚事务
            if (status != null) {
                transactionManager.rollback(status);
            }
            throw e;
        }
    }
}

2.2 @Transactional的属性解析

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
    // 事务管理器bean名称
    String value() default "";
    
    // 传播行为
    Propagation propagation() default Propagation.REQUIRED;
    
    // 隔离级别  
    Isolation isolation() default Isolation.DEFAULT;
    
    // 超时时间
    int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
    
    // 是否只读
    boolean readOnly() default false;
    
    // 回滚异常类
    Class<? extends Throwable>[] rollbackFor() default {};
    
    // 不回滚异常类
    Class<? extends Throwable>[] noRollbackFor() default {};
}

三、8种常见的@Transactional失效场景及解决方案

场景1:自调用问题(最常见)

问题描述​:在同一个类中,一个方法调用另一个@Transactional方法。

@Service
public class UserService {
    
    public void updateUserInfo(User user) {
        // 一些业务逻辑
        updateUserStatus(user.getId(), "ACTIVE"); // 自调用,事务失效!
    }
    
    @Transactional
    public void updateUserStatus(Long userId, String status) {
        userMapper.updateStatus(userId, status);
        // 这里抛出异常不会回滚上面的updateUserInfo操作
        if (status.equals("ACTIVE")) {
            throw new RuntimeException("测试异常");
        }
    }
}

根源分析​:自调用绕过代理机制,直接调用原始方法。

解决方案​:

  1. 将事务方法移到另一个Service
  2. 通过AopContext获取代理对象
@Service
public class UserService {
    
    public void updateUserInfo(User user) {
        // 获取代理对象
        UserService proxy = (UserService) AopContext.currentProxy();
        proxy.updateUserStatus(user.getId(), "ACTIVE"); // 通过代理调用
    }
}

需要在启动类添加​​:@EnableAspectJAutoProxy(exposeProxy = true)

场景2:异常被捕获未抛出

问题描述​:在事务方法中捕获异常但未重新抛出。

@Service
public class OrderService {
    
    @Transactional
    public void createOrder(Order order) {
        try {
            orderMapper.insert(order);
            // 可能抛出异常的操作
            inventoryService.deductInventory(order);
        } catch (Exception e) {
            // 只是记录日志,异常被"吃掉"了,事务不会回滚!
            log.error("创建订单失败", e);
        }
    }
}

根源分析​:Spring事务基于异常回滚,捕获异常后框架无法感知。

解决方案​:

  1. 在catch块中抛出异常
  2. 手动设置回滚
@Transactional
public void createOrder(Order order) {
    try {
        orderMapper.insert(order);
        inventoryService.deductInventory(order);
    } catch (Exception e) {
        log.error("创建订单失败", e);
        // 方案1:抛出异常
        throw new RuntimeException("订单创建失败", e);
        // 方案2:手动回滚
        // TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
}

场景3:异常类型非RuntimeException

问题描述​:抛出受检异常(Checked Exception)但未配置回滚规则。

@Service
public class FileService {
    
    @Transactional
    public void processFile(String filePath) throws IOException {
        fileMapper.insertFileRecord(filePath);
        // 受检异常,默认不会回滚事务!
        if (!Files.exists(Paths.get(filePath))) {
            throw new IOException("文件不存在");
        }
    }
}

根源分析​:Spring默认只对RuntimeException和Error回滚。

解决方案​:明确指定回滚异常类型

@Transactional(rollbackFor = Exception.class)  // 所有异常都回滚
public void processFile(String filePath) throws IOException {
    // ...
}

场景4:方法非public修饰

问题描述​:@Transactional注解在非public方法上。

@Service
public class InternalService {
    
    @Transactional
    void internalUpdate(User user) {  // 包级访问,事务失效!
        userMapper.update(user);
        logMapper.insertLog(user);
    }
}

根源分析​:Spring AOP基于代理,无法代理非public方法。

解决方案​:将方法改为public,或使用AspectJ模式

public class InternalService {
    
    @Transactional
    public void internalUpdate(User user) {  // 改为public
        // ...
    }
}

场景5:数据库引擎不支持事务

问题描述​:使用MyISAM等不支持事务的存储引擎。

-- 检查表引擎
SHOW TABLE STATUS LIKE 'user_table';

-- 如果是MyISAM,事务注解无效
-- Engine: MyISAM

根源分析​:事务是数据库层面的特性,存储引擎必须支持。

解决方案​:改为InnoDB引擎

ALTER TABLE user_table ENGINE=InnoDB;

场景6:多数据源下未指定事务管理器

问题描述​:多数据源环境下未明确指定事务管理器。

@Configuration
public class DataSourceConfig {
    
    @Bean
    @Primary
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create().build();
    }
    
    @Bean
    public DataSource secondaryDataSource() {
        return DataSourceBuilder.create().build();
    }
}

@Service
public class CrossDataSourceService {
    
    @Transactional  // 使用哪个事务管理器?可能不是我们期望的
    public void crossUpdate() {
        primaryUserMapper.update();  // 数据源1
        secondaryOrderMapper.insert(); // 数据源2
    }
}

根源分析​:多数据源时需要明确指定事务管理器。

解决方案​:使用@Transactional的value属性指定

@Transactional("primaryTransactionManager")
public void updatePrimary() {
    primaryUserMapper.update();
}

@Transactional("secondaryTransactionManager")  
public void updateSecondary() {
    secondaryOrderMapper.insert();
}

// 或者使用事务同步管理器处理多数据源事务
@Transactional("primaryTransactionManager")
public void crossUpdate() {
    primaryUserMapper.update();
    TransactionSynchronizationManager.registerSynchronization(
        new TransactionSynchronization() {
            @Override
            public void afterCommit() {
                // 在主事务提交后执行次数据源操作
                secondaryOrderMapper.insert();
            }
        }
    );
}

场景7、传播行为配置不当

问题描述​:错误使用PROPAGATION\_NOT\_SUPPORTED等传播行为。

@Service
public class PropagationService {
    
    @Transactional(propagation = Propagation.REQUIRED)
    public void outerMethod() {
        userMapper.updateUser();
        innerMethod();  // 内层方法挂起外部事务!
    }
    
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void innerMethod() {
        // 这个方法将以非事务方式执行
        logMapper.insertLog();
    }
}

根源分析​:NOT\_SUPPORTED会挂起当前事务,以非事务方式执行。

解决方案​:根据业务需求选择合适的传播行为

// 常用传播行为说明
public enum Propagation {
    REQUIRED,      // 支持当前事务,不存在则创建新事务
    REQUIRES_NEW,  // 创建新事务,挂起当前事务
    NESTED,        // 嵌套事务
    SUPPORTS,      // 支持当前事务,不存在则以非事务运行
    NOT_SUPPORTED, // 以非事务方式执行,挂起当前事务
    NEVER,         // 以非事务方式执行,存在事务则抛异常
    MANDATORY      // 必须在事务中运行,否则抛异常
}

场景8、异步方法中使用@Transactional

问题描述​:在@Async方法中使用事务注解。

@Service  
public class AsyncService {
    
    @Async
    @Transactional
    public void asyncUpdate(User user) {
        // 这个方法会在新线程中执行,事务可能不生效!
        userMapper.update(user);
    }
}

根源分析​:异步方法在新线程执行,事务上下文无法传递。

解决方案​:将事务边界放在调用异步方法的地方

@Service
public class AsyncWrapService {
    
    @Transactional
    public void wrappedAsyncUpdate(User user) {
        // 同步方法中处理事务
        userMapper.updateSome(user);
        
        // 异步执行非核心操作
        asyncService.asyncUpdate(user);
    }
}

四、深度排查:事务失效的调试技巧

4.1 开启事务调试日志

# application.properties
logging.level.org.springframework.transaction.interceptor=TRACE
logging.level.org.springframework.orm.jpa=DEBUG
logging.level.org.springframework.jdbc.datasource.DataSourceTransactionManager=DEBUG

4.2 使用TransactionSynchronizationManager调试

@Transactional
public void debugTransaction() {
    System.out.println("当前是否有活动事务: " + 
        TransactionSynchronizationManager.isActualTransactionActive());
    
    System.out.println("当前事务名称: " + 
        TransactionSynchronizationManager.getCurrentTransactionName());
        
    System.out.println("事务隔离级别: " + 
        TransactionSynchronizationManager.getCurrentTransactionIsolationLevel());
}

4.3 自定义TransactionSynchronization

@Transactional
public void withTransactionSync() {
    TransactionSynchronizationManager.registerSynchronization(
        new TransactionSynchronization() {
            @Override
            public void afterCommit() {
                System.out.println("事务提交后执行");
            }
            
            @Override
            public void afterCompletion(int status) {
                System.out.println("事务完成,状态: " + status);
            }
        }
    );
}

五、最佳实践:避免事务失效的编程规范

基于四年经验,总结出以下最佳实践:

5.1 事务方法设计原则

  1. 单一职责​:事务方法应该只完成一个完整的业务操作
  2. 短小精悍​:事务执行时间要尽可能短
  3. 避免远程调用​:事务内避免RPC、HTTP等不稳定操作

5.2 代码组织规范

@Service
@Transactional  // 类级别注解,所有public方法默认支持事务
public class UserService {
    
    // 只读操作使用只读事务
    @Transactional(readOnly = true)
    public User findById(Long id) {
        return userMapper.selectById(id);
    }
    
    // 写操作使用读写事务
    public void updateUser(User user) {
        validateUser(user);
        userMapper.update(user);
        // 其他必须在同一事务中的操作
    }
    
    // 内部方法不应该直接调用,用protected或private限制
    protected void validateUser(User user) {
        // 验证逻辑
    }
}

5.3 异常处理规范

@Service
public class OrderService {
    
    @Transactional(rollbackFor = Exception.class)
    public void createOrder(Order order) {
        try {
            // 核心业务逻辑
            processOrder(order);
        } catch (BusinessException e) {
            // 业务异常,记录日志但不回滚
            log.warn("业务异常,订单创建失败", e);
            throw e;
        } catch (Exception e) {
            // 系统异常,记录错误日志并回滚
            log.error("系统异常,订单创建失败", e);
            throw new SystemException("订单创建失败", e);
        }
    }
}

六、高级话题:分布式事务的考量

在微服务架构下,@Transactional的局限性更加明显:

// 分布式事务场景 - @Transactional无法解决
@Service
public class DistributedService {
    
    @Transactional
    public void distributedOperation() {
        // 本地数据库操作
        localMapper.update();
        
        // 远程服务调用 - 不在同一个事务中!
        feignClient.remoteUpdate();
        
        // 如果这里抛出异常,本地操作会回滚,但远程操作不会!
        throw new RuntimeException("异常");
    }
}

解决方案​:使用Seata、消息队列等分布式事务方案。

总结

@Transactional注解的失效问题本质上是对Spring AOP和事务传播机制理解不足导致的。四年经验让我明白了几个关键点:

  1. 代理机制是基础​:理解JDK动态代理和CGLIB代理的区别
  2. 异常驱动回滚​:事务回滚是由异常触发的
  3. 传播行为要谨慎​:不同的传播行为适用于不同场景
  4. 自调用要避免​:这是最常见的陷阱

真正的解决方案不是记住所有失效场景,而是深入理解Spring事务的工作原理​。当遇到事务问题时,能够从代理机制、异常处理、传播行为等角度系统分析,这才是中级开发者应该具备的能力。

这次深入探究不仅解决了当时的数据不一致问题,更重要的是建立了一套完整的事务问题排查方法论,这在后续的微服务架构演进中发挥了重要作用。

版权声明 ▶ 本网站名称:黄磊的博客
▶ 本文标题:@Transactional失效的8种场景及根源剖析
▶ 本文链接:https://www.huangleicole.com/backend-related/48.html
▶ 转载本站文章需要遵守:商业转载请联系站长,非商业转载请注明出处!!

如果觉得我的文章对你有用,请随意赞赏