为什么你的 @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》

相关推荐
搜狐技术产品小编20235 分钟前
告别“黑盒”与误判:如何用“多智能体对抗辩论”重构内容安全审核系统
后端·多agent
snow@li10 分钟前
HikariCP:高性能数据库连接池全景深入梳理
java·数据库
DantyWei30 分钟前
controller注册及调用时机
后端
edwarddamon1 小时前
Spring Cloud 配置热更新与 Bean 代理机制梳理
java·后端
码事漫谈1 小时前
我用 Seed Evolving 做了个 AI 小说写作工具
后端
Raas1001 小时前
MAIGateway,魔芋企业级AI网关的FinAPI成本竞争力设计
java·后端·网关·api网关·魔芋ai·finapi·mai gateway
evans在进步2 小时前
Spring AI 从入门到实战:用 Java 实现大模型对话与 Tool Calling
java·人工智能·spring
evans在进步2 小时前
LeetCode 189 轮转数组:三次反转原地解决,图解 Java 实现
java·算法·leetcode
DantyWei2 小时前
kube-controller-manager的leader选举流程和策略
后端