Spring Boot多数据源切换:三种实现方式详解与实战

发布于:2025-06-16 ⋅ 阅读:(30) ⋅ 点赞:(0)

在复杂业务系统中,多数据源切换已成为必备技能。本文将深入剖析三种主流实现方案,带你从入门到精通!

一、多数据源应用场景

  1. 读写分离:主库负责写操作,从库处理读请求

  2. 多租户系统:不同租户使用独立数据库

  3. 分库分表:业务数据按规则分散存储

  4. 多数据库类型:同时使用MySQL、Oracle等异构数据库

二、3种实现方案对比

方案 实现复杂度 侵入性 维护成本 适用场景
AbstractRoutingDataSource 中等 简单读写分离
多SqlSessionFactory 异构数据库
dynamic-datasource 复杂多数据源

三、方案一:AbstractRoutingDataSource

实现原理

通过继承Spring的AbstractRoutingDataSource类,动态路由数据源

实现步骤

1. 添加依赖
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.27</version>
    </dependency>
</dependencies>

 

2. 配置多数据源

# application.yml
spring:
  datasource:
    master:
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://localhost:3306/master_db
      username: root
      password: root123
    slave:
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://localhost:3306/slave_db
      username: root
      password: root123
3. 动态数据源配置类
@Configuration
public class DataSourceConfig {
    
    @Bean
    @ConfigurationProperties("spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }
    
    @Bean
    @ConfigurationProperties("spring.datasource.slave")
    public DataSource slaveDataSource() {
        return DataSourceBuilder.create().build();
    }
    
    @Bean
    public DataSource dynamicDataSource() {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("master", masterDataSource());
        targetDataSources.put("slave", slaveDataSource());
        
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        dynamicDataSource.setTargetDataSources(targetDataSources);
        dynamicDataSource.setDefaultTargetDataSource(masterDataSource());
        return dynamicDataSource;
    }
}
4. 自定义动态数据源
public class DynamicDataSource extends AbstractRoutingDataSource {
    
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSourceKey();
    }
    
    public static class DataSourceContextHolder {
        private static final ThreadLocal<String> contextHolder = 
            new ThreadLocal<>();
        
        public static void setDataSourceKey(String key) {
            contextHolder.set(key);
        }
        
        public static String getDataSourceKey() {
            return contextHolder.get();
        }
        
        public static void clearDataSourceKey() {
            contextHolder.remove();
        }
    }
}
5. 自定义注解切换数据源
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
    String value() default "master";
}
6. AOP切面实现动态切换
@Aspect
@Component
public class DataSourceAspect {
    
    @Before("@annotation(dataSource)")
    public void beforeSwitchDataSource(JoinPoint point, DataSource dataSource) {
        String dataSourceKey = dataSource.value();
        DynamicDataSource.DataSourceContextHolder.setDataSourceKey(dataSourceKey);
    }
    
    @After("@annotation(dataSource)")
    public void afterSwitchDataSource(JoinPoint point, DataSource dataSource) {
        DynamicDataSource.DataSourceContextHolder.clearDataSourceKey();
    }
}
7. 业务层使用示例
@Service
public class UserService {
    
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    // 使用主库
    @DataSource("master")
    public void createUser(User user) {
        String sql = "INSERT INTO users(name, email) VALUES(?, ?)";
        jdbcTemplate.update(sql, user.getName(), user.getEmail());
    }
    
    // 使用从库
    @DataSource("slave")
    public List<User> getAllUsers() {
        String sql = "SELECT * FROM users";
        return jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class));
    }
}

方案优缺点

优点

  • 纯Spring实现,无第三方依赖

  • 灵活控制数据源切换

缺点

  • 事务管理复杂

  • 需手动处理连接池

  • 切换逻辑侵入业务代码

四、方案二:多SqlSessionFactory

实现原理

为每个数据源创建独立的MyBatis SqlSessionFactory

实现步骤

1. 主数据源配置
@Configuration
@MapperScan(basePackages = "com.example.mapper.master", 
           sqlSessionFactoryRef = "masterSqlSessionFactory")
public class MasterDataSourceConfig {
    
    @Bean
    @ConfigurationProperties("spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }
    
    @Bean
    public SqlSessionFactory masterSqlSessionFactory(
            @Qualifier("masterDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        bean.setMapperLocations(
            new PathMatchingResourcePatternResolver()
                .getResources("classpath:mapper/master/*.xml"));
        return bean.getObject();
    }
    
    @Bean
    public DataSourceTransactionManager masterTransactionManager(
            @Qualifier("masterDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}
2. 从数据源配置
@Configuration
@MapperScan(basePackages = "com.example.mapper.slave", 
           sqlSessionFactoryRef = "slaveSqlSessionFactory")
public class SlaveDataSourceConfig {
    
    @Bean
    @ConfigurationProperties("spring.datasource.slave")
    public DataSource slaveDataSource() {
        return DataSourceBuilder.create().build();
    }
    
    @Bean
    public SqlSessionFactory slaveSqlSessionFactory(
            @Qualifier("slaveDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        bean.setMapperLocations(
            new PathMatchingResourcePatternResolver()
                .getResources("classpath:mapper/slave/*.xml"));
        return bean.getObject();
    }
    
    @Bean
    public DataSourceTransactionManager slaveTransactionManager(
            @Qualifier("slaveDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}
3. 业务层使用
@Service
public class OrderService {
    
    // 注入主库Mapper
    @Autowired
    @Qualifier("masterOrderMapper")
    private OrderMapper masterOrderMapper;
    
    // 注入从库Mapper
    @Autowired
    @Qualifier("slaveOrderMapper")
    private OrderMapper slaveOrderMapper;
    
    @Transactional(transactionManager = "masterTransactionManager")
    public void createOrder(Order order) {
        masterOrderMapper.insert(order);
    }
    
    @Transactional(transactionManager = "slaveTransactionManager")
    public Order getOrder(Long id) {
        return slaveOrderMapper.selectById(id);
    }
}

方案优缺点

优点

  • 各数据源完全隔离

  • 事务管理清晰

  • 支持异构数据库

缺点

  • 配置复杂,冗余代码多

  • Mapper需按数据源分包

  • 动态切换不灵活

五、方案三:dynamic-datasource框架

框架优势

  • 零侵入:通过注解实现数据源切换

  • 功能丰富:支持读写分离、分库分表等

  • 简单易用:简化多数据源配置

实现步骤

1. 添加依赖
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
    <version>3.5.0</version>
</dependency>
2. 配置数据源
spring:
  datasource:
    dynamic:
      primary: master # 默认数据源
      strict: false   # 是否严格匹配数据源
      datasource:
        master:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://localhost:3306/master_db
          username: root
          password: root123
        slave1:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://localhost:3306/slave_db1
          username: root
          password: root123
        slave2:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://localhost:3306/slave_db2
          username: root
          password: root123
        oracle_db:
          driver-class-name: oracle.jdbc.OracleDriver
          url: jdbc:oracle:thin:@localhost:1521:orcl
          username: system
          password: oracle123
3. 使用@DS注解切换数据源
@Service
public class ProductService {

    // 默认使用主库
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    // 使用主库
    @DS("master")
    public void createProduct(Product product) {
        jdbcTemplate.update("INSERT INTO product(...) VALUES(...)");
    }
    
    // 随机使用从库
    @DS("slave")
    public Product getProduct(Long id) {
        return jdbcTemplate.queryForObject(
            "SELECT * FROM product WHERE id = ?", 
            new BeanPropertyRowMapper<>(Product.class), id);
    }
    
    // 指定特定从库
    @DS("slave1")
    public List<Product> getHotProducts() {
        return jdbcTemplate.query(
            "SELECT * FROM product WHERE hot = 1", 
            new BeanPropertyRowMapper<>(Product.class));
    }
    
    // 使用Oracle数据库
    @DS("oracle_db")
    public List<Category> getOracleCategories() {
        return jdbcTemplate.query(
            "SELECT * FROM categories", 
            new BeanPropertyRowMapper<>(Category.class));
    }
}
4. 高级功能:读写分离
spring:
  datasource:
    dynamic:
      primary: master
      datasource:
        master:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://master-host:3306/db
          username: root
          password: root123
        slave_1:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://slave1-host:3306/db
          username: root
          password: root123
        slave_2:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://slave2-host:3306/db
          username: root
          password: root123
      strategy: # 读写分离配置
        load-balance: # 负载均衡策略
          slave: round_robin # 从库轮询策略
5. 事务管理
@DS("master")
@Transactional
public void placeOrder(Order order) {
    // 1. 扣减库存
    productService.reduceStock(order.getProductId(), order.getQuantity());
    
    // 2. 创建订单
    orderMapper.insert(order);
    
    // 3. 记录日志
    logService.logOrder(order);
    
    // 所有操作都在主库事务中执行
}

最佳实践技巧

  1. 数据源分组管理

    spring:
      datasource:
        dynamic:
          datasource:
            master_1: 
              # 配置...
            master_2:
              # 配置...
            slave_1:
              # 配置...
            slave_2:
              # 配置...
          group:
            masters: master_1, master_2
            slaves: slave_1, slave_2
  2. 多租户数据源动态注册

    @Autowired
    private DynamicRoutingDataSource routingDataSource;
    
    public void addTenantDataSource(String tenantId, DataSourceProperty property) {
        DataSource dataSource = dataSourceCreator.createDataSource(property);
        routingDataSource.addDataSource(tenantId, dataSource);
    }
  3. 自定义负载均衡策略

    public class RandomStrategy implements LoadBalanceStrategy {
        @Override
        public String determineDataSource(
            List<String> dataSourceNames) {
            Random random = new Random();
            return dataSourceNames.get(
                random.nextInt(dataSourceNames.size()));
        }
    }

六、性能优化建议

  1. 连接池配置优化

    spring:
      datasource:
        dynamic:
          datasource:
            master:
              # ...
              hikari:
                maximum-pool-size: 20
                minimum-idle: 5
                connection-timeout: 30000
                idle-timeout: 600000
                max-lifetime: 1800000
  2. 避免频繁切换数据源

    • 将同一数据源操作集中处理

    • 使用@DSTransactional管理跨库事务

  3. 监控数据源状态

    @RestController
    public class DataSourceMonitor {
        
        @Autowired
        private DynamicRoutingDataSource routingDataSource;
        
        @GetMapping("/datasources")
        public Map<String, DataSource> listDataSources() {
            return routingDataSource.getDataSources();
        }
        
        @GetMapping("/datasources/stats")
        public Map<String, Object> getDataSourceStats() {
            Map<String, Object> stats = new HashMap<>();
            routingDataSource.getDataSources().forEach((key, ds) -> {
                if(ds instanceof HikariDataSource) {
                    HikariDataSource hikari = (HikariDataSource) ds;
                    HikariPoolMXBean pool = hikari.getHikariPoolMXBean();
                    stats.put(key, Map.of(
                        "active", pool.getActiveConnections(),
                        "idle", pool.getIdleConnections(),
                        "total", pool.getTotalConnections()
                    ));
                }
            });
            return stats;
        }
    }

七、方案选型建议

  1. 中小型项目:优先选用dynamic-datasource,开发效率高

  2. 异构数据库系统:选择多SqlSessionFactory方案,隔离性好

  3. 需要高度定制:AbstractRoutingDataSource提供最大灵活性

  4. 云原生环境:dynamic-datasource + Seata分布式事务

八、常见问题解决方案

  1. 数据源切换失效

    • 检查方法是否被AOP代理

    • 确保@DS注解在public方法上

    • 避免类内部方法调用

  2. 跨库事务问题

    // 使用分布式事务
    @DS("order")
    @GlobalTransactional
    public void createOrder(Order order) {
        // 操作订单库
        orderMapper.insert(order);
        
        // 操作库存库
        stockService.reduce(order.getProductId(), order.getQuantity());
    }
  3. 连接泄露检测

    @Bean
    public DataSource dataSource(DataSourceProperties properties) {
        HikariDataSource dataSource = properties.initializeDataSourceBuilder()
            .type(HikariDataSource.class).build();
        dataSource.setLeakDetectionThreshold(5000); // 5秒泄露检测
        return dataSource;
    }

九、结语

多数据源管理是现代应用开发的必备技能。通过本文介绍的三种方案:

  1. AbstractRoutingDataSource:Spring原生方案,适合定制化场景

  2. 多SqlSessionFactory:适合异构数据库系统

  3. dynamic-datasource:生产环境首选,功能强大易用

最佳实践提示:对于大多数Java项目,推荐使用dynamic-datasource框架,它提供了一站式的解决方案,大大降低了多数据源管理的复杂度。同时结合Spring Cloud Alibaba Seata,可轻松实现分布式事务管理。

扩展阅读

掌握多数据源切换技术,让你的应用从容应对复杂数据场景!

 


网站公告

今日签到

点亮在社区的每一天
去签到