全部学科
Python全栈
python
NodeJS全栈
nodejs
小程序首页
📅 2026-05-20 8 分钟 ✍️ juanwangdev

动态数据源切换

多数据源架构允许应用在不同场景下路由到不同数据库,实现读写分离、跨库查询、租户隔离等需求。本文基于 AbstractRoutingDataSource 结合 MyBatis 实现完整的动态数据源方案。

核心原理

Java
数据源路由流程
┌─────────────┐
│  业务代码    │
│  setRoute()  │
└──────┬──────┘
       │ ThreadLocal 存储路由键
       ↓
┌──────────────────────────┐
│  RoutingDataSource        │
│  determineCurrentLookupKey│  ← 返回路由键 (master/slave/tenant_x)
└──────┬───────────────────┘
       │ 查找已注册数据源
       ↓
┌──────────────────────────┐
│  DataSource Map           │
│  master → HikariDS_1     │
│  slave1 → HikariDS_2     │
│  slave2 → HikariDS_3     │
└──────┬───────────────────┘
       │ 返回实际 Connection
       ↓
┌─────────────┐
│  SQL 执行    │
└─────────────┘

AbstractRoutingDataSource 是 Spring 提供的多数据源抽象类,核心方法 determineCurrentLookupKey() 返回当前应使用的数据源键值。

读写分离实现

路由键定义

Java
public class DataSourceContextHolder {

    private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();

    public static final String MASTER = "master";
    public static final String SLAVE = "slave";

    public static void setDataSource(String ds) {
        CONTEXT.set(ds);
    }

    public static String getDataSource() {
        return CONTEXT.get() != null ? CONTEXT.get() : MASTER;
    }

    public static void clear() {
        CONTEXT.remove();
    }
}

路由数据源

Java
public class ReadWriteSplittingDataSource extends AbstractRoutingDataSource {

    private int slaveCount = 0;
    private final AtomicInteger counter = new AtomicInteger(0);

    public ReadWriteSplittingDataSource(DataSource master, List<DataSource> slaves) {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DataSourceContextHolder.MASTER, master);

        for (int i = 0; i < slaves.size(); i++) {
            targetDataSources.put(DataSourceContextHolder.SLAVE + i, slaves.get(i));
        }
        slaveCount = slaves.size();

        setTargetDataSources(targetDataSources);
        setDefaultTargetDataSource(master);
        afterPropertiesSet();
    }

    @Override
    protected Object determineCurrentLookupKey() {
        String route = DataSourceContextHolder.getDataSource();

        // 轮询选择 slave
        if (DataSourceContextHolder.SLAVE.equals(route) && slaveCount > 0) {
            int index = Math.abs(counter.getAndIncrement() % slaveCount);
            return DataSourceContextHolder.SLAVE + index;
        }

        return DataSourceContextHolder.MASTER;
    }
}

AOP 自动路由

YAML
@Aspect
@Component
@Order(-1)  // 确保在事务切面之前执行
public class DataSourceAspect {

    @Pointcut("execution(* com.example..mapper.*.*(..))")
    public void mapperPointcut() {}

    @Before("mapperPointcut()")
    public void before(JoinPoint point) {
        String methodName = point.getSignature().getName();

        // 读操作自动路由到 slave
        if (methodName.startsWith("select") || methodName.startsWith("find")
                || methodName.startsWith("get") || methodName.startsWith("list")
                || methodName.startsWith("count") || methodName.startsWith("query")) {
            DataSourceContextHolder.setDataSource(DataSourceContextHolder.SLAVE);
        } else {
            // 写操作路由到 master
            DataSourceContextHolder.setDataSource(DataSourceContextHolder.MASTER);
        }
    }

    @After("mapperPointcut()")
    public void after() {
        DataSourceContextHolder.clear();
    }
}

Spring Boot 配置

Java
spring:
  datasource:
    master:
      url: jdbc:mysql://master-host:3306/app_db
      username: root
      password: xxx
      driver-class-name: com.mysql.cj.jdbc.Driver
    slaves:
      - url: jdbc:mysql://slave1-host:3306/app_db
        username: read_user
        password: xxx
      - url: jdbc:mysql://slave2-host:3306/app_db
        username: read_user
        password: xxx
Java
@Configuration
public class DataSourceConfig {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    public List<DataSource> slaveDataSources(
            @Value("${spring.datasource.slaves[0].url}") String slave1Url,
            @Value("${spring.datasource.slaves[0].username}") String slave1User,
            @Value("${spring.datasource.slaves[0].password}") String slave1Pwd,
            @Value("${spring.datasource.slaves[1].url}") String slave2Url,
            @Value("${spring.datasource.slaves[1].username}") String slave2User,
            @Value("${spring.datasource.slaves[1].password}") String slave2Pwd) {

        List<DataSource> slaves = new ArrayList<>();
        slaves.add(buildDataSource(slave1Url, slave1User, slave1Pwd));
        slaves.add(buildDataSource(slave2Url, slave2User, slave2Pwd));
        return slaves;
    }

    private DataSource buildDataSource(String url, String user, String pwd) {
        return DataSourceBuilder.create()
                .url(url)
                .username(user)
                .password(pwd)
                .build();
    }

    @Bean
    public ReadWriteSplittingDataSource routingDataSource(
            DataSource master, List<DataSource> slaves) {
        return new ReadWriteSplittingDataSource(master, slaves);
    }

    @Bean
    public SqlSessionFactory sqlSessionFactory(ReadWriteSplittingDataSource ds) throws Exception {
        SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
        factory.setDataSource(ds);
        factory.setMapperLocations(
            new PathMatchingResourcePatternResolver()
                .getResources("classpath*:mapper/**/*.xml"));
        return factory.getObject();
    }
}

手动指定数据源

自定义注解

Java
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
    String value() default DataSourceContextHolder.MASTER;
}
Java
@Aspect
@Component
@Order(-1)
public class ManualDataSourceAspect {

    @Before("@annotation(dataSource)")
    public void before(DataSource dataSource) {
        DataSourceContextHolder.setDataSource(dataSource.value());
    }

    @After("@annotation(dataSource)")
    public void after(DataSource dataSource) {
        DataSourceContextHolder.clear();
    }
}
Java
public interface OrderMapper {

    // 自动路由到 slave(select 前缀)
    List<Order> selectOrders();

    // 手动指定数据源
    @DataSource("slave")
    Order selectFromBackup(@Param("id") Long id);

    // 强制 master
    @DataSource("master")
    int forceMasterUpdate(@Param("id") Long id, @Param("status") String status);
}

事务内数据源一致性

读写分离场景中,同一事务内必须使用同一数据源,避免主从延迟导致的数据不一致。

Java
事务内数据源一致性
┌────────────────────────────────────┐
│  @Transactional                     │
│    select() → 从库 (旧数据)  ❌      │
│    update() → 主库 (新数据)         │
│    select() → 从库 (仍未更新) ❌     │
│                                     │
│  正确做法:整个事务使用主库           │
│    update() → 主库                  │
│    select() → 主库 (读到最新)  ✓     │
└────────────────────────────────────┘

事务同步方案

Java
@Aspect
@Component
@Order(-1)
public class TransactionAwareDataSourceAspect {

    @Before("@annotation(dataSource)")
    public void before(DataSource dataSource) {
        // 如果当前已在事务中,强制使用 master
        if (TransactionSynchronizationManager.isActualTransactionActive()) {
            DataSourceContextHolder.setDataSource(DataSourceContextHolder.MASTER);
        } else {
            DataSourceContextHolder.setDataSource(dataSource.value());
        }
    }

    @After("@annotation(dataSource)")
    public void after(DataSource dataSource) {
        if (!TransactionSynchronizationManager.isActualTransactionActive()) {
            DataSourceContextHolder.clear();
        }
    }
}

同一事务内,Spring 复用同一 Connection。AbstractRoutingDataSource 在首次 determineCurrentLookupKey() 后缓存结果,事务内不会切换。

多数据源与 MyBatis 集成要点

SqlSessionTemplate 配置

Java
@Bean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
    // ExecutorType.SIMPLE 适用于读写分离场景
    return new SqlSessionTemplate(sqlSessionFactory, ExecutorType.SIMPLE);
}

多数据源独立 SqlSessionFactory

当不同数据源需要完全不同的 Mapper 配置时,可创建多个独立的 SqlSessionFactory:

text
@Configuration
public class MultiDataSourceConfig {

    @Bean(name = "orderSqlSessionFactory")
    public SqlSessionFactory orderSqlSessionFactory(
            @Qualifier("orderDataSource") DataSource ds) throws Exception {
        SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
        factory.setDataSource(ds);
        factory.setMapperLocations(
            new PathMatchingResourcePatternResolver()
                .getResources("classpath*:mapper/order/**/*.xml"));
        return factory.getObject();
    }

    @Bean(name = "userSqlSessionFactory")
    public SqlSessionFactory userSqlSessionFactory(
            @Qualifier("userDataSource") DataSource ds) throws Exception {
        SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
        factory.setDataSource(ds);
        factory.setMapperLocations(
            new PathMatchingResourcePatternResolver()
                .getResources("classpath*:mapper/user/**/*.xml"));
        return factory.getObject();
    }
}

动态数据源监控

text
@Component
public class DataSourceMonitor {

    @Autowired
    private ReadWriteSplittingDataSource routingDs;

    /** 获取当前线程使用的数据源 */
    public String getCurrentDataSource() {
        return DataSourceContextHolder.getDataSource();
    }

    /** 获取所有已注册数据源 */
    public Map<Object, Object> getRegisteredDataSources() {
        return routingDs.getResolvedDataSources();
    }

    /** 切换记录 */
    public void logSwitch(String from, String to) {
        // 接入监控系统:Prometheus / SkyWalking
    }
}

注意事项

  1. 主从延迟:写操作后立即读取可能读到旧数据,重要场景强制读主库
  2. 事务内不切换:同一事务内必须使用同一数据源,Spring 会自动复用 Connection
  3. AOP 顺序:数据源切换 AOP 的 @Order 必须小于事务 AOP 的 @Order,确保先切换数据源再开事务
  4. 连接池独立:主从库应使用独立的连接池配置,slave 可使用只读用户降低权限
  5. ThreadLocal 清理:请求结束后必须在 finally@After 中清理 ThreadLocal,防止线程复用导致数据源泄漏

要点总结

  • AbstractRoutingDataSource 通过 determineCurrentLookupKey() 返回路由键,动态选择目标数据源
  • 读写分离通过 AOP 根据方法名前缀自动路由:select/find/get → slave,update/insert/delete → master
  • 多 slave 场景可使用轮询策略实现读负载均衡
  • 事务内必须保证数据源一致性,通过 TransactionSynchronizationManager.isActualTransactionActive() 判断
  • 手动指定数据源使用自定义 @DataSource 注解,通过 AOP 在方法执行前设置路由键
  • 多套独立 Mapper 需创建多个 SqlSessionFactory,各自绑定不同数据源和 XML 路径
  • ThreadLocal 必须在请求结束后清理,AOP 顺序必须在事务切面之前

存放路径:D:\git2\jwdev\articles\MYBATIS\专家\多租户与数据权限\动态数据源切换.md

📝 发现内容有误?点击此处直接编辑

← 上一篇 数据脱敏插件
下一篇 → 多租户架构设计
想查看更多题目和详细解析?
小程序提供完整的题库、模拟考试和详细解析
马上就来

长按或扫描二维码,立即体验

扫码体验小程序
马上就来
使用微信扫描二维码
立即体验完整题库