为什么你的 @Transactional 不生效?一文搞懂 Spring 事务机制

大家好,我是大华。 相信很多朋友在使用@Transactional事务注解时都踩过坑,有时候代码看起来没问题,但事务就是不生效,或者出现了莫名其妙的问题。

什么是事务?

在深入代码之前,我们先理解事务的ACID特性:

1.原子性(Atomicity) :事务中的所有操作要么全部完成,要么全部不完成,不会结束在中间某个环节 2.一致性(Consistency) :事务执行前后,数据库的完整性约束不被破坏 3.隔离性(Isolation) :并发事务之间互不干扰,每个事务都感觉不到其他事务在并发执行 4.持久性(Durability):事务完成后,对数据的修改是永久的,即使系统故障也不会丢失

举个生活中的例子:银行转账就是典型的事务场景 - A向B转账100元需要两步:

  1. A账户减100元
  2. B账户加100元

这两步必须同时成功同时失败,绝对不能出现A的钱扣了但B没收到的情况。这就是事务要解决的核心问题!

@Transactional 基础用法

先来看一个简单的事务使用示例:

java 复制代码
@Service
public class UserService {
    
    @Autowired
    private UserRepository userRepository;
    
    /**
     * 最简单的事务用法
     * 方法内所有数据库操作要么全部成功,要么全部回滚
     */
    @Transactional
    public void transferMoney(Long fromUserId, Long toUserId, BigDecimal amount) {
        // 第一步:从转出方扣款
        User fromUser = userRepository.findById(fromUserId)
            .orElseThrow(() -> new RuntimeException("转出用户不存在"));
        if (fromUser.getBalance().compareTo(amount) < 0) {
            throw new RuntimeException("余额不足");
        }
        fromUser.setBalance(fromUser.getBalance().subtract(amount));
        userRepository.save(fromUser);
        
        // 第二步:向接收方转账
        User toUser = userRepository.findById(toUserId)
            .orElseThrow(() -> new RuntimeException("接收用户不存在"));
        toUser.setBalance(toUser.getBalance().add(amount));
        userRepository.save(toUser);
        
        // 如果任何一步出现异常,整个操作都会回滚
        log.info("转账成功:{} 向 {} 转账 {}", fromUserId, toUserId, amount);
    }
}

这个例子中,如果扣款成功但转账失败,Spring会自动回滚整个操作,确保数据一致性。

坑1:事务不生效的经典场景

Spring的事务管理是通过AOP代理实现的。当在同一个类中调用被@Transactional注解的方法时,调用的是原始对象的方法,而不是代理对象的方法,因此事务拦截器不会生效。

问题代码示例

java 复制代码
@Service
public class ProblematicUserService {
    
    @Autowired
    private UserRepository userRepository;
    
    public void updateUserWithProblem(User user) {
        // 这里直接调用同类方法,事务注解不会生效
        updateUser(user); // 事务失效!
    }
    
    @Transactional
    public void updateUser(User user) {
        userRepository.save(user);
        // 即使这里抛出异常,数据也不会回滚
        if (user.getAge() < 0) {
            throw new IllegalArgumentException("年龄不能为负数");
        }
    }
}

在同一个类内部调用,事务不生效!这是因为Spring AOP代理的机制导致的。

解决方案1:通过ApplicationContext获取代理对象

java 复制代码
@Service
public class Solution1UserService {
    
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private ApplicationContext applicationContext;
    
    public void updateUserCorrectly(User user) {
        // 关键:从Spring容器中获取代理对象
        Solution1UserService proxy = applicationContext.getBean(Solution1UserService.class);
        proxy.updateUser(user); // 现在事务生效了
    }
    
    @Transactional
    public void updateUser(User user) {
        userRepository.save(user);
        if (user.getAge() < 0) {
            throw new IllegalArgumentException("年龄不能为负数");
        }
    }
}

通过ApplicationContext获取代理对象,从Spring容器中获取代理对象,确保事务生效。

解决方案2:将事务方法拆分到不同Service中(推荐)

首先创建专门处理事务的Service:

java 复制代码
/**
 * 专门处理事务的Service
 * 推荐使用这种方式,代码结构更清晰
 */
@Service
public class TransactionalService {
    
    @Autowired
    private UserRepository userRepository;
    
    /**
     * 事务方法放在独立的Service中
     */
    @Transactional
    public void updateUserInTransaction(User user) {
        userRepository.save(user);
        if (user.getAge() < 0) {
            throw new IllegalArgumentException("年龄不能为负数");
        }
    }
}

然后在原Service中注入并使用:

java 复制代码
@Service
public class Solution2UserService {
    
    @Autowired
    private TransactionalService transactionalService;
    
    public void updateUserByOtherService(User user) {
        // 调用专门的事务Service,确保事务生效
        transactionalService.updateUserInTransaction(user);
    }
}

调用其他Service的事务方法,这种方式代码更清晰,也符合单一职责原则。

为什么解决方案2更推荐?

1. 代码清晰 :事务方法集中在专门的Service中,职责明确 2. 易于维护 :事务相关的修改只影响一个Service 3. 避免循环依赖 :拆分Service可以减少复杂的依赖关系 4. 符合设计原则:单一职责原则,每个类都有明确的职责

坑2:异常类型不对导致不回滚

Spring默认只对RuntimeException及其子类进行回滚,对受检异常(Exception)不回滚。 这是因为受检异常通常表示可恢复的业务异常,而运行时异常表示不可恢复的系统异常。

问题代码示例

java 复制代码
@Service
public class ExceptionProblemService {
    
    @Autowired
    private UserRepository userRepository;
    
    /**
     * 问题:默认只对RuntimeException回滚,Exception不会回滚
     */
    @Transactional
    public void updateUserWithExceptionProblem(User user) throws Exception {
        userRepository.save(user);
        
        if (user.getName() == null) {
            // 这里抛出的是Exception,不是RuntimeException,默认不会回滚!
            throw new Exception("用户名不能为空"); // 不会触发回滚!
        }
    }
}

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

java 复制代码
@Service
public class ExceptionSolution1Service {
    
    @Autowired
    private UserRepository userRepository;
    
    @Transactional(rollbackFor = Exception.class)
    public void updateUserWithCorrectExceptionHandling(User user) throws Exception {
        userRepository.save(user);
        
        if (user.getName() == null) {
            // 现在这个异常也会触发回滚了
            throw new Exception("用户名不能为空");
        }
    }
}

明确指定回滚的异常类型,使用rollbackFor指定所有Exception都回滚

解决方案2:使用RuntimeException(推荐)

java 复制代码
@Service
public class ExceptionSolution2Service {
    
    @Autowired
    private UserRepository userRepository;
    
    /**
     * 方案2:使用RuntimeException(推荐)
     * 业务异常可以继承RuntimeException
     */
    @Transactional
    public void updateUserWithRuntimeException(User user) {
        userRepository.save(user);
        
        if (user.getName() == null) {
            // RuntimeException默认会回滚
            throw new BusinessException("用户名不能为空");
        }
    }
}

/**
 * 自定义业务异常基类,继承RuntimeException
 */
class BusinessException extends RuntimeException {
    public BusinessException(String message) {
        super(message);
    }
}

业务异常可以继承RuntimeException实现。

坑3:事务传播机制理解不清

Spring定义了7种事务传播行为,最常用的是: 1. REQUIRED (默认):如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务 2. REQUIRES_NEW :创建一个新的事务,如果当前存在事务,则把当前事务挂起 3. NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行

问题代码示例

java 复制代码
@Service
public class PropagationProblemService {
    
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private OrderRepository orderRepository;
    
    /**
     * 外层方法:开启一个事务
     */
    @Transactional
    public void outerMethod(User user, Order order) {
        // 这个保存操作在外层事务中
        userRepository.save(user);
        
        try {
            // 调用内层方法
            innerMethod(order);
        } catch (Exception e) {
            // 由于内层方法使用默认的REQUIRED传播机制,
            // 它加入了外层事务,所以内层异常会导致外层事务也回滚
            // user的保存会被回滚!
            log.error("内层方法异常,外层事务也会回滚", e);
        }
    }
    
    /**
     * 内层方法:默认使用REQUIRED传播机制
     * 加入外层事务,同一个事务
     */
    @Transactional(propagation = Propagation.REQUIRED)
    public void innerMethod(Order order) {
        orderRepository.save(order);
        throw new RuntimeException("订单保存失败,整个事务回滚");
    }
}

正确使用传播机制的示例

java 复制代码
@Service
public class PropagationSolutionService {
    
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private OrderRepository orderRepository;
    
    @Transactional
    public void correctOuterMethod(User user, Order order) {
        // 用户保存操作 - 这个在外层事务中
        userRepository.save(user);
        
        try {
            // 使用REQUIRES_NEW:新建独立事务,不影响外层事务
            correctInnerMethod(order);
        } catch (Exception e) {
            // 内层事务回滚,但外层事务继续执行
            log.error("内层事务回滚,但外层事务不受影响", e);
        }
        
        // 这里可以继续其他操作,不受内层事务失败影响
        log.info("用户保存成功,继续执行其他业务逻辑");
    }
    
    /**
     * 内层方法:使用REQUIRES_NEW传播机制
     * 创建新事务,独立于外层事务
     */
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void correctInnerMethod(Order order) {
        orderRepository.save(order);
        // 这个异常只会回滚内层事务
        throw new RuntimeException("订单保存失败,只回滚内层事务");
    }
}

根据业务需求选择合适的传播机制,这里希望内层事务不影响外层事务。

完整的最佳实践示例

下面是一个综合了所有最佳实践的完整示例:

java 复制代码
@Service
@Slf4j
public class BestPracticeService {
    
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private OrderRepository orderRepository;
    
    @Autowired
    private OperationLogRepository operationLogRepository;
    
    /**
     * 完整的事务最佳实践示例
     * @param request 订单请求
     * @return 处理结果
     */
    @Transactional(
        rollbackFor = Exception.class, // 所有异常都回滚
        timeout = 30,                  // 设置30秒超时,防止长时间占用连接
        isolation = Isolation.DEFAULT, // 使用数据库默认隔离级别
        propagation = Propagation.REQUIRED, // 默认传播机制
        readOnly = false               // 读写事务
    )
    public CompleteResult processUserOrder(OrderRequest request) {
        log.info("开始处理用户订单事务");
        
        // 1. 验证用户信息
        User user = validateUser(request.getUserId());
        
        // 2. 扣减账户余额
        deductUserBalance(user, request.getAmount());
        
        // 3. 创建订单记录
        Order order = createOrder(user, request);
        
        // 4. 记录操作日志(这个方法应该不在事务中)
        logOperation(user, order);
        
        log.info("用户订单处理完成");
        return new CompleteResult(user, order);
    }
    
    /**
     * 验证用户信息
     */
    private User validateUser(Long userId) {
        return userRepository.findById(userId)
                .orElseThrow(() -> new BusinessException("用户不存在"));
    }
    
    /**
     * 扣减用户余额
     */
    private void deductUserBalance(User user, BigDecimal amount) {
        if (user.getBalance().compareTo(amount) < 0) {
            throw new BusinessException("用户余额不足");
        }
        user.setBalance(user.getBalance().subtract(amount));
        userRepository.save(user);
        log.info("用户 {} 余额扣减 {}", user.getId(), amount);
    }
    
    /**
     * 创建订单
     */
    private Order createOrder(User user, OrderRequest request) {
        Order order = new Order();
        order.setAmount(request.getAmount());
        order.setStatus("COMPLETED");
        Order savedOrder = orderRepository.save(order);
        log.info("创建订单成功,订单ID: {}", savedOrder.getId());
        return savedOrder;
    }
    
    /**
     * 日志记录通常不需要事务,使用NOT_SUPPORTED传播机制
     * 这样即使日志记录失败,也不会影响主业务流程
     */
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void logOperation(User user, Order order) {
        try {
            OperationLog log = new OperationLog();
            log.setOperationType("CREATE_ORDER");
            operationLogRepository.save(log);
            log.info("操作日志记录成功");
        } catch (Exception e) {
            // 日志记录失败不应该影响主业务流程
            log.error("记录操作日志失败,但不影响主流程", e);
        }
    }
}

/**
 * 自定义业务异常
 */
class BusinessException extends RuntimeException {
    public BusinessException(String message) {
        super(message);
    }
}

/**
 * 请求DTO
 */
@Data
class OrderRequest {
    private Long userId;
    private BigDecimal amount;
    private String productId;
}

/**
 * 返回结果DTO  
 */
@Data
@AllArgsConstructor
class CompleteResult {
    private User user;
    private Order order;
}

事务配置建议

java 复制代码
@Configuration
@EnableTransactionManagement
public class TransactionConfig {
    
    /**
     * 事务管理最佳实践配置
     */
    @Bean
    public TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) {
        TransactionTemplate template = new TransactionTemplate(transactionManager);
        template.setTimeout(30); // 设置合理的超时时间
        template.setReadOnly(false);
        template.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
        template.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        return template;
    }
}

总结

不适合使用事务的场景:

查询操作 :纯查询不需要事务,可以添加@Transactional(readOnly = true) 耗时较长的业务 :长时间占用数据库连接会影响性能 非数据库操作:如调用外部接口、文件操作等(这些操作无法被数据库事务管理)

事务选择建议:

简单查询 :不加事务或使用readOnly = true 单表修改 :可以使用事务,但要根据业务重要性决定 多表关联修改 :必须使用事务 重要业务操作:必须使用事务,并做好异常处理

关键要点:

1、方法必须是public的,事务才生效 2、避免同类方法调用,使用代理对象调用 3、异常要正确处理,不要随意捕获异常 4、了解传播机制,根据业务需求选择合适的传播行为 5、做好参数校验和异常处理,保证代码健壮性

事务不是越多越好,也不是越大越好,合理使用才是关键!

本文首发于公众号:程序员刘大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!

📌往期精彩

《这20条SQL优化方案,让你的数据库查询速度提升10倍》

《MySQL 为什么不推荐用雪花ID 和 UUID 做主键?》

《无需UI库!50行CSS打造丝滑弹性动效导航栏,拿来即用》

《SpringBoot3+Vue3实现的数据库文档工具,自动生成Markdown/HTML》

相关推荐
Lacrimosa&L8 小时前
OS_3 Memory、4 File、5 IO
java
逻极8 小时前
Rust 结构体方法(Methods):为数据附加行为
开发语言·后端·rust
爱学的小码8 小时前
JavaEE——多线程1(超详细版)
java·java-ee
国服第二切图仔8 小时前
Rust入门开发之Rust 集合:灵活的数据容器
开发语言·后端·rust
今日说"法"8 小时前
Rust 线程安全性的基石:Send 与 Sync 特性解析
开发语言·后端·rust
tuokuac8 小时前
依赖spring-cloud-starter-gateway与spring-cloud-gateway-dependencies的区别
java·gateway
seabirdssss8 小时前
JDK 11 环境正确,端口未被占用,但是运行 Tomcat 11 仍然闪退
java·开发语言·tomcat
努力学算法的蒟蒻8 小时前
day03(11.1)——leetcode面试经典150
java·算法·leetcode
Mr YiRan9 小时前
SYN关键字辨析,各种锁优缺点分析和面试题讲解
java·开发语言