事务

为什么需要事务

转账这类操作由「扣款 + 入账」多步组成,任何一步失败都应整体撤销,否则账目不平。事务把一组操作打包成原子单元:全部成功才提交,任一失败就回滚。Spring 的声明式事务用 @Transactional 声明边界,由代理在方法前后自动开启、提交或回滚,业务代码零样板。

开启方式

Spring Boot 检测到事务管理器后会自动开启事务管理(spring-boot-starter-jdbcspring-boot-starter-data-jpa 都会带来事务管理器),直接使用 @Transactional 即可,无需手工加 @EnableTransactionManagement

基本用法

@Service
public class TransferService {

    @Transactional(rollbackFor = Exception.class)
    public void transfer(Long fromId, Long toId, BigDecimal amount) {
        accountDao.decrease(fromId, amount);
        accountDao.increase(toId, amount);
        // 任一语句抛异常,两步一起回滚
    }
}

要点:

  • 可加在类上(该类所有 public 方法生效),方法级注解优先;
  • 只对 public 方法生效(通过代理实现,非 public 方法不经过代理);
  • 默认只对 RuntimeExceptionError 回滚;抛受检异常时不会回滚,需要显式 rollbackFor = Exception.class
  • 常用属性还有 isolation(隔离级别)、timeout(超时秒数)、readOnly = true(只读事务,适合批量查询场景);语义细节以官方文档为准。

传播行为

方法调用链上已有事务时,「是否新建、是否加入」由传播属性决定,常用三种:

传播行为典型场景
REQUIRED(默认)有事务则加入,没有则新建大多数业务方法
REQUIRES_NEW挂起当前事务,另起新事务操作日志落库,失败不影响主事务
NESTED嵌套保存点,内层回滚只回退到保存点分批处理中某批失败不影响已成功批次

其余 SUPPORTS/NOT_SUPPORTED/MANDATORY/NEVER 了解即可,完整语义以官方文档为准。

常见失效场景

  1. 自调用:同类方法里 this.xxx() 直接调用,不经代理,事务不生效。解决:把需要事务的方法放进另一个 Bean,或注入自身的代理。
  2. 异常被吞:方法内 try-catch 把异常吃掉,事务管理器看不到异常自然不会回滚。应让异常向上抛出,或只捕获后重新抛出。
  3. 非 public 方法@Transactional 不生效。
  4. 多线程:事务方法内新开线程执行的代码没有事务上下文,不在同一事务内。

多数据源场景

多数据源时 Spring 的自动配置会后退,需要自己声明数据源与事务管理器,并指定主数据源:

@Configuration
public class DataSourceConfig {

    @Bean
    @Primary
    @ConfigurationProperties("spring.datasource.primary")
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties("spring.datasource.secondary")
    public DataSource secondaryDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @Primary
    public DataSourceTransactionManager primaryTx(DataSource primaryDataSource) {
        return new DataSourceTransactionManager(primaryDataSource);
    }

    @Bean
    public DataSourceTransactionManager secondaryTx(DataSource secondaryDataSource) {
        return new DataSourceTransactionManager(secondaryDataSource);
    }
}

对应的配置键(Hikari 使用 jdbc-url 约定)与 DataSourceBuilder 的行为细节以官方文档为准。使用时按事务管理器名指定:

@Transactional("secondaryTx")   // 默认主事务管理器时可不写
public void writeSecondary() { ... }

注意:普通数据库事务只保证单库原子性。跨库、跨系统操作需要分布式事务(如 Seata、本地消息表等方案),超出本教程范围,请查阅对应方案的官方文档。

小结

@Transactional 用好了是一行注解的事,用坏了是「假事务」。记住三条:public + 代理入口 + 让异常抛出来;多数据源时各配各的事务管理器、用注解名区分。

笔记加载中…