一、问题背景

在Spring Boot项目中,多数据源配置是一个常见的需求场景,比如读写分离、分库分表、对接多个业务数据库等。通常的实现方式是通过继承AbstractRoutingDataSource,结合AOP切面或注解(如@DS)来实现数据源的动态切换。

然而,在实际开发中,开发者经常会遇到一个棘手的问题:@Transactional事务注解与动态数据源切换同时存在时,数据源切换失效或事务不生效。本文将从源码层面深入分析原因,并结合事务传播机制给出完整的解决方案。


二、多数据源事务失效的常见场景

2.1 场景一:@Transactional + @DS 注解同用导致数据源切换失效

这是最常见的场景。在Service层的方法上同时标注了@DS("slave")@Transactional,期望切换到从库执行并开启事务,结果却发现操作的是主库。

@Service
public class UserServiceImpl implements UserService {
    
    @DS("slave")  // 期望切换到从库
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void queryFromSlave() {
        // 实际执行时,操作的是主库!
        userMapper.selectList();
    }
}

2.2 场景二:多个数据源各自配置事务管理器,但事务管理器未正确指定

当项目中存在多个DataSourceTransactionManager时,Spring默认只会使用@Primary标注的事务管理器,或者按类型获取唯一的一个。如果方法上未指定transactionManager,则所有操作都会走默认事务管理器对应的数据源。

// 错误写法:未指定事务管理器
@Transactional(rollbackFor = Exception.class)
public void updateDb2() {
    // 实际使用的是默认数据源(主库)的事务管理器
}

2.3 场景三:同一个事务中切换多个数据源

在一个@Transactional方法内部,先后操作数据源A和数据源B,期望两者在同一个事务中。但实际上,Spring的事务管理器在开启事务时就已经绑定了连接,后续切换数据源无法生效。

@Transactional
public void crossDbOperation() {
    // 操作主库
    masterMapper.insert(record1);
    
    // 尝试切换到从库 —— 实际上仍然使用主库连接!
    DataSourceContextHolder.set("slave");
    slaveMapper.insert(record2);
    
    // 抛出异常时,只有主库回滚,从库已提交
    throw new RuntimeException("rollback");
}

2.4 场景四:AOP切面顺序问题导致数据源切换在事务之后执行

@Transactional本身也是通过AOP实现的。如果数据源切换切面的@Order优先级低于事务切面的优先级,那么事务开启时会先执行(此时数据源尚未切换),导致使用的是默认数据源。


三、深入源码:事务失效的根本原因

3.1 事务与连接绑定的核心机制

Spring事务管理的核心是将数据库连接(Connection)与当前线程绑定。关键类是TransactionSynchronizationManager,它内部使用ThreadLocal<Map<Object, Object>>来缓存资源:

public abstract class TransactionSynchronizationManager {
    private static final ThreadLocal<Map<Object, Object>> resources =
            new NamedThreadLocal<>("Transactional resources");
}

当开启事务时,DataSourceTransactionManager.doBegin()方法会获取数据库连接,并将其绑定到当前线程:

// DataSourceTransactionManager.doBegin()
Connection con = dataSource.getConnection();
ConnectionHolder holderToUse = new ConnectionHolder(con);
TransactionSynchronizationManager.bindResource(dataSource, holderToUse);

后续在同一个事务中的所有SQL操作,都会通过DataSourceUtils.doGetConnection()获取连接。该方法会先从线程缓存中查找:

public static Connection doGetConnection(DataSource dataSource) throws SQLException {
    // 从事务管理器中获取connection,如果有直接使用
    ConnectionHolder conHolder = (ConnectionHolder) 
        TransactionSynchronizationManager.getResource(dataSource);
    if (conHolder != null && conHolder.hasConnection()) {
        return conHolder.getConnection();
    }
    // 否则新建连接...
}

关键问题getResource(dataSource)中的dataSource是作为Map的Key。当使用AbstractRoutingDataSource时,传入的dataSource始终是同一个DynamicDataSource代理对象,所以缓存的连接也始终是同一个。即使后续determineCurrentLookupKey()返回了不同的数据源Key,由于连接已经缓存,不会再重新获取。

3.2 @Transactional 与 @DS 的执行顺序问题

@Transactional@DS都是通过AOP实现的。Spring AOP的拦截顺序由@Order值决定,值越小优先级越高。

@Transactional 的执行链路:
TransactionInterceptor.intercept() 
→ TransactionAspectSupport.createTransactionIfNecessary()
→ AbstractPlatformTransactionManager.getTransaction()
→ DataSourceTransactionManager.doBegin()
→ AbstractRoutingDataSource.determineTargetDataSource()  // 此时lookupKey可能为null!
→ 获取默认数据源 → 缓存连接

如果@Transactional的AOP先执行(默认Ordered.LOWEST_PRECEDENCE),而@DS的AOP后执行,那么doBegin()执行时,ThreadLocal中的数据源Key尚未设置,只能拿到默认数据源。

3.3 调用链路总结

场景 调用链路 结果
无事务 Mapper → SqlSession → Executor → getConnection() → DynamicDataSource → 根据ThreadLocal Key获取对应数据源 ✅ 正常切换
有事务 @Transactional → doBegin() → 获取默认连接并缓存 → Mapper执行 → 复用缓存连接 ❌ 始终使用默认数据源

四、与事务传播机制的关联分析

4.1 传播机制在多数据源下的表现

Spring定义了7种事务传播行为,在多数据源场景下,它们的表现与单数据源有很大不同:

4.1.1 REQUIRED(默认)
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
    // 操作主库
    masterMapper.insert();
    
    methodB();  // 调用另一个方法
}

@DS("slave")
@Transactional(propagation = Propagation.REQUIRED)
public void methodB() {
    // 期望操作从库,但实际复用了methodA的事务和连接
    slaveMapper.insert();
}

表现methodB会加入methodA的事务,复用主库的连接。即使methodB上有@DS("slave"),数据源切换也不会生效,因为事务已经开启,连接已缓存。

4.1.2 REQUIRES_NEW
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
    masterMapper.insert();
    methodB();
}

@DS("slave")
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void methodB() {
    slaveMapper.insert();
}

表现methodB会挂起当前事务,创建一个新事务。创建新事务时会重新调用doBegin()获取连接。如果此时@DS的AOP已经执行(设置了ThreadLocal Key),那么determineCurrentLookupKey()会返回正确的Key,从而获取到从库连接。

重要局限REQUIRES_NEW只是创建了两个独立的事务,它们之间没有原子性。如果methodA回滚,methodB已经提交的数据不会回滚,造成数据不一致。

4.1.3 其他传播行为
传播行为 多数据源下的适用性 说明
SUPPORTS ⚠️ 慎用 有事务则加入,无事务则以非事务执行。数据源切换在无事务时正常,有事务时失效
MANDATORY ❌ 不推荐 要求必须存在事务,在多数据源下事务上下文混乱
NOT_SUPPORTED ⚠️ 特殊场景 以非事务方式执行,可以正常切换数据源,但无事务保护
NEVER ❌ 不推荐 要求不能有事务,多数据源下几乎无适用场景
NESTED ❌ 不支持 基于Savepoint的嵌套事务,不支持跨数据源

4.2 传播机制的正确使用姿势

在多数据源场景下,传播机制的正确理解至关重要:

  1. 同一数据源内:使用REQUIRED(默认)即可,事务正常生效

  2. 跨数据源操作REQUIRES_NEW可以让数据源切换生效,但牺牲原子性

  3. 需要原子性:必须使用JTA分布式事务或柔性事务方案,传播机制本身无法解决


五、解决方案

5.1 方案一:调整AOP切面顺序(解决@DS + @Transactional冲突)

将数据源切换切面的优先级调高,确保在事务开启之前就完成数据源切换。

@Aspect
@Component
@Order(-1)  // 优先级高于@Transactional(默认Ordered.LOWEST_PRECEDENCE)
public class DataSourceAspect {
    
    @Pointcut("@annotation(com.example.annotation.DS)")
    public void dsPointCut() {}
    
    @Before("dsPointCut() && @annotation(ds)")
    public void before(JoinPoint point, DS ds) {
        String dataSourceKey = ds.value();
        DataSourceContextHolder.set(dataSourceKey);
        log.info("切换数据源至:{}", dataSourceKey);
    }
    
    @After("dsPointCut()")
    public void after(JoinPoint point) {
        DataSourceContextHolder.clear();
    }
}

注意:此方法只能解决"第一次切换"的问题。如果在同一个事务中再次切换数据源,由于连接已缓存,仍然无效。

5.2 方案二:为每个数据源配置独立的事务管理器

这是最基本也是最重要的配置。每个数据源必须有自己的DataSourceTransactionManager,使用时明确指定。

@Configuration
public class DataSourceConfig {
    
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }
    
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.slave")
    public DataSource slaveDataSource() {
        return DataSourceBuilder.create().build();
    }
    
    // 动态数据源
    @Bean
    @Primary
    public DataSource dynamicDataSource(
            @Qualifier("masterDataSource") DataSource master,
            @Qualifier("slaveDataSource") DataSource slave) {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        Map<Object, Object> target = new HashMap<>();
        target.put("master", master);
        target.put("slave", slave);
        dynamicDataSource.setTargetDataSources(target);
        dynamicDataSource.setDefaultTargetDataSource(master);
        return dynamicDataSource;
    }
    
    // 主库事务管理器
    @Bean("masterTxManager")
    @Primary
    public DataSourceTransactionManager masterTxManager(
            @Qualifier("masterDataSource") DataSource ds) {
        return new DataSourceTransactionManager(ds);
    }
    
    // 从库事务管理器
    @Bean("slaveTxManager")
    public DataSourceTransactionManager slaveTxManager(
            @Qualifier("slaveDataSource") DataSource ds) {
        return new DataSourceTransactionManager(ds);
    }
}

使用时明确指定:

@Service
public class UserServiceImpl implements UserService {
    
    // 操作从库,使用从库事务管理器
    @Transactional(transactionManager = "slaveTxManager", rollbackFor = Exception.class)
    public void updateSlave() {
        slaveMapper.update();
    }
    
    // 操作主库,使用主库事务管理器
    @Transactional(transactionManager = "masterTxManager", rollbackFor = Exception.class)
    public void updateMaster() {
        masterMapper.update();
    }
}

注意:此方案要求操作的是单一数据源,跨数据源操作仍然无法保证原子性。

5.3 方案三:使用REQUIRES_NEW实现跨数据源操作(非原子性)

当必须在同一个方法中操作多个数据源时,可以将操作拆分到不同方法,并使用REQUIRES_NEW强制开启新事务。

@Service
public class OrderServiceImpl implements OrderService {
    
    @Autowired
    private OrderService self;  // 注入自身代理对象
    
    @Transactional(transactionManager = "masterTxManager", rollbackFor = Exception.class)
    public void createOrder(Order order) {
        // 1. 主库:保存订单
        orderMapper.insert(order);
        
        // 2. 从库:记录日志 —— 必须通过代理调用,否则AOP不生效
        self.saveLog(order);
        
        // 注意:如果这里抛出异常,主库回滚,但从库的日志已提交!
    }
    
    @DS("slave")
    @Transactional(transactionManager = "slaveTxManager", 
                   propagation = Propagation.REQUIRES_NEW, 
                   rollbackFor = Exception.class)
    public void saveLog(Order order) {
        logMapper.insert(order);
    }
}

关键要点

  1. 必须通过代理对象调用(self.saveLog()),直接this.saveLog()不会触发AOP

  2. REQUIRES_NEW会挂起当前事务,创建新事务,从而获取新的连接

  3. 数据一致性风险:两个独立事务,不具备原子性

5.4 方案四:自定义TransactionFactory修复连接缓存(MyBatis场景)

当使用MyBatis-Plus等框架时,可以通过自定义TransactionFactory来管理多个连接,绕过Spring默认的连接缓存机制。

public class MultiDataSourceTransactionFactory extends SpringManagedTransactionFactory {
    
    @Override
    public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {
        return new MultiDataSourceTransaction(dataSource);
    }
}

public class MultiDataSourceTransaction implements Transaction {
    
    private final DynamicDataSource dynamicDataSource;
    private final Map<String, Connection> connectionMap = new ConcurrentHashMap<>();
    
    public MultiDataSourceTransaction(DataSource dataSource) {
        this.dynamicDataSource = (DynamicDataSource) dataSource;
    }
    
    @Override
    public Connection getConnection() throws SQLException {
        String dsKey = DataSourceContextHolder.get();
        // 每个数据源独立缓存连接
        return connectionMap.computeIfAbsent(dsKey, k -> {
            try {
                DataSource ds = dynamicDataSource.getActualDataSource(k);
                return DataSourceUtils.getConnection(ds);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        });
    }
    
    @Override
    public void commit() throws SQLException {
        for (Connection conn : connectionMap.values()) {
            if (conn != null && !conn.isClosed() && !conn.getAutoCommit()) {
                conn.commit();
            }
        }
    }
    
    @Override
    public void rollback() throws SQLException {
        for (Connection conn : connectionMap.values()) {
            if (conn != null && !conn.isClosed() && !conn.getAutoCommit()) {
                conn.rollback();
            }
        }
    }
    
    @Override
    public void close() throws SQLException {
        for (Connection conn : connectionMap.values()) {
            DataSourceUtils.releaseConnection(conn, dynamicDataSource);
        }
        connectionMap.clear();
    }
}

然后在配置中注册:

@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
    SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
    bean.setDataSource(dataSource);
    bean.setTransactionFactory(new MultiDataSourceTransactionFactory());
    return bean.getObject();
}

原理:不再依赖Spring的TransactionSynchronizationManager缓存,而是自己管理每个数据源对应的连接,在commit/rollback时统一处理。

5.5 方案五:JTA分布式事务(强一致性)

如果需要真正的跨数据源原子性,必须使用JTA(Java Transaction API)实现XA两阶段提交。

<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
@Configuration
public class XADataSourceConfig {
    
    @Bean
    public UserTransactionManager userTransactionManager() {
        return new UserTransactionManager();
    }
    
    @Bean
    public JtaTransactionManager jtaTransactionManager() {
        return new JtaTransactionManager(
            new UserTransactionImp(), 
            userTransactionManager()
        );
    }
    
    @Bean
    public DataSource masterDataSource() {
        AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
        ds.setXaDataSourceClassName("com.mysql.cj.jdbc.MysqlXADataSource");
        ds.setUniqueResourceName("master");
        // ... 其他配置
        return ds;
    }
    
    @Bean
    public DataSource slaveDataSource() {
        AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
        ds.setXaDataSourceClassName("com.mysql.cj.jdbc.MysqlXADataSource");
        ds.setUniqueResourceName("slave");
        // ... 其他配置
        return ds;
    }
}

使用:

@Transactional(transactionManager = "jtaTransactionManager", rollbackFor = Exception.class)
public void crossDbOperation() {
    masterMapper.insert(record1);  // 数据源A
    slaveMapper.insert(record2);   // 数据源B
    // 异常时两者都回滚,真正的原子性
}

特点:强一致性,但性能开销大(两阶段提交),适合金融等对一致性要求极高的场景。

5.6 方案六:柔性事务(最终一致性)

对于性能敏感、一致性要求不那么严格的场景,可以采用TCC或Saga模式。

// Saga模式伪代码
public void business() {
    SagaInstance saga = sagaManager.create();
    try {
        saga.execute(stepA);  // 数据源A本地事务
        saga.execute(stepB);  // 数据源B本地事务
    } catch (Exception e) {
        saga.compensate();  // 触发补偿
    }
}

六、总结与最佳实践

6.1 问题速查表

表格

问题现象 根本原因 解决方案
@DS + @Transactional 数据源不切换 AOP顺序问题 + 连接缓存 调整AOP @Order(-1),或拆分到不同方法
多个数据源事务不生效 未指定事务管理器 为每个数据源配置独立TransactionManager,使用时明确指定
同一个事务中切换数据源无效 Spring事务连接与线程绑定,缓存后不复用 拆分为独立事务(REQUIRES_NEW),或使用JTA
跨数据源操作数据不一致 Spring原生不支持跨数据源原子性 使用JTA分布式事务或柔性事务

6.2 最佳实践

  1. 明确指定事务管理器:多数据源环境下,@Transactional必须指定transactionManager,这是最基本的要求。

  2. 避免同一事务跨数据源:不要在同一个@Transactional方法内操作多个数据源,这是架构上的反模式。如果业务需要,考虑服务拆分。

  3. 慎用REQUIRES_NEW:虽然它能让数据源切换生效,但会创建独立事务,牺牲原子性。只在明确接受数据不一致风险时使用。

  4. AOP顺序要正确:如果使用@DS等注解切换数据源,确保切面优先级高于事务切面(@Order值更小)。

  5. 连接管理要清理:使用完ThreadLocal中的数据源Key后,务必在@After中清理,防止线程复用导致数据源错乱。

  6. 日志排查:开启MyBatis和Spring事务的DEBUG日志,观察实际使用的数据源和连接对象。

  7. 考虑架构演进:如果频繁需要跨数据源事务,说明系统耦合度过高,应考虑微服务拆分,通过Seata等分布式事务框架解决。

更多推荐