SpringBoot多数据源实战:用JdbcTemplate+dynamic-datasource实现读写分离(附完整配置)

在企业级应用开发中,数据库性能往往是系统瓶颈所在。当单台数据库服务器无法满足高并发读写需求时,读写分离架构便成为提升系统吞吐量的有效方案。本文将深入探讨如何基于SpringBoot生态,结合JdbcTemplate与dynamic-datasource组件,构建一套灵活高效的读写分离解决方案。

1. 读写分离架构设计原理

读写分离的核心思想是将数据库的读操作和写操作分发到不同的服务器节点。主库(Master)负责处理所有写操作(INSERT/UPDATE/DELETE),从库(Slave)则承担读操作(SELECT)的负载。这种架构通过分散I/O压力,显著提升系统整体性能。

典型应用场景包括:

  • 电商平台的商品浏览(读多)
  • 内容管理系统的文章发布(写少)
  • 金融系统的交易记录查询(读密集)

实现读写分离需要解决两个关键问题:

  1. 数据同步:主库变更如何实时同步到从库
  2. 路由决策:如何自动将操作路由到正确的数据库

MySQL通过主从复制(Replication)机制解决第一个问题,而本文重点讲解第二个问题在SpringBoot中的实现方案。

2. 技术选型与环境准备

2.1 组件对比

方案优点缺点
AbstractRoutingDataSource原生支持,无需额外依赖需要手动维护数据源上下文
ShardingSphere功能全面,支持分库分表学习曲线陡峭,配置复杂
dynamic-datasource轻量易用,注解驱动事务管理需要特殊处理

2.2 依赖配置

在pom.xml中添加必要依赖:

<dependencies>
    <!-- SpringBoot Starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    
    <!-- 动态数据源核心库 -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
        <version>3.5.1</version>
    </dependency>
    
    <!-- MySQL驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>

提示:建议使用最新稳定版dynamic-datasource,老版本可能存在已知问题。

3. 多数据源配置实战

3.1 YAML配置详解

application.yml中配置主从数据源:

spring:
  datasource:
    dynamic:
      primary: master # 默认数据源
      strict: true    # 严格匹配数据源
      datasource:
        master:
          url: jdbc:mysql://master-host:3306/db?useSSL=false
          username: root
          password: Master@123
          driver-class-name: com.mysql.cj.jdbc.Driver
        slave1:
          url: jdbc:mysql://slave1-host:3306/db?useSSL=false
          username: read_user
          password: Read@123
          driver-class-name: com.mysql.cj.jdbc.Driver
        slave2:
          url: jdbc:mysql://slave2-host:3306/db?useSSL=false
          username: read_user
          password: Read@123
          driver-class-name: com.mysql.cj.jdbc.Driver

关键参数说明:

  • primary:设置默认数据源,当未指定数据源时使用
  • strict:设置为true时,未匹配的数据源将抛出异常

3.2 数据源健康检查

为确保系统稳定性,建议添加健康检查配置:

@Configuration
public class DataSourceHealthConfig {
    
    @Bean
    public DataSourceHealthIndicator dynamicDataSourceHealthIndicator(
            DynamicRoutingDataSource dataSource) {
        return new DataSourceHealthIndicator(dataSource);
    }
}

4. 数据源路由实现方案

4.1 注解驱动方式

@DS注解是dynamic-datasource提供的核心功能,支持类级别和方法级别的数据源指定:

@Repository
public class OrderRepository {
    
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    // 使用从库查询
    @DS("slave1")
    public List<Order> findRecentOrders() {
        String sql = "SELECT * FROM orders ORDER BY create_time DESC LIMIT 10";
        return jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Order.class));
    }
    
    // 使用主库写入
    @DS("master")
    public void createOrder(Order order) {
        String sql = "INSERT INTO orders(...) VALUES(...)";
        jdbcTemplate.update(sql, order.getParams());
    }
}

4.2 编程式路由

对于需要动态决策的场景,可以使用DynamicDataSourceContextHolder:

public List<Product> searchProducts(String keyword) {
    // 根据负载情况动态选择从库
    String selectedSlave = loadBalanceSelectSlave();
    DynamicDataSourceContextHolder.push(selectedSlave);
    
    try {
        String sql = "SELECT * FROM products WHERE name LIKE ?";
        return jdbcTemplate.query(sql, 
            new Object[]{"%" + keyword + "%"},
            new BeanPropertyRowMapper<>(Product.class));
    } finally {
        DynamicDataSourceContextHolder.clear();
    }
}

负载均衡策略示例:

private String loadBalanceSelectSlave() {
    // 简单轮询实现
    AtomicInteger counter = new AtomicInteger(0);
    List<String> slaves = Arrays.asList("slave1", "slave2");
    return slaves.get(counter.getAndIncrement() % slaves.size());
}

5. 事务管理特别处理

多数据源环境下的事务管理需要特别注意,默认的Spring事务管理器无法跨数据源工作。以下是解决方案:

5.1 本地事务配置

@Configuration
public class TransactionConfig {
    
    @Bean
    @DS("master")
    public PlatformTransactionManager masterTxManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

5.2 分布式事务建议

对于需要跨数据源的事务操作,建议:

  1. 使用最终一致性方案(如消息队列)
  2. 考虑引入Seata等分布式事务框架
  3. 设计补偿机制处理异常情况

注意:@DS注解在事务方法内切换数据源无效,必须在事务开始前确定数据源。

6. 性能优化实践

6.1 从库负载均衡

在application.yml中配置从库负载均衡:

spring:
  datasource:
    dynamic:
      strategy: loadBalance # 启用负载均衡策略
      lb:
        enabled: true
        slaveRegex: ^slave.* # 匹配从库名称的正则

6.2 连接池调优

为不同数据源配置独立的连接池参数:

spring:
  datasource:
    dynamic:
      datasource:
        master:
          hikari:
            maximum-pool-size: 20
            connection-timeout: 30000
        slave1:
          hikari:
            maximum-pool-size: 30
            connection-timeout: 10000

6.3 SQL监控配置

集成Druid监控多数据源:

@Configuration
public class DruidConfig {

    @Bean
    @DS("master")
    public ServletRegistrationBean<StatViewServlet> druidServlet() {
        ServletRegistrationBean<StatViewServlet> reg = new ServletRegistrationBean<>();
        reg.setServlet(new StatViewServlet());
        reg.addUrlMappings("/druid/*");
        return reg;
    }
}

7. 常见问题排查

问题1:数据源切换不生效

  • 检查@DS注解是否被AOP代理拦截
  • 确认方法访问修饰符不是private
  • 验证是否在事务方法内尝试切换

问题2:主从数据不一致

  • 检查MySQL主从复制状态:SHOW SLAVE STATUS
  • 设置合理的复制延迟阈值
  • 关键业务考虑强制读主库

问题3:连接池耗尽

  • 检查连接泄漏:SELECT * FROM information_schema.PROCESSLIST
  • 调整连接池大小
  • 设置合理的超时时间

在实际项目中,我们曾遇到从库延迟导致的查询结果不一致问题。最终通过引入@DS("#header.slaveNearest")注解,配合中间件计算从库延迟,自动选择延迟最低的从库,将问题发生率降低了90%。

更多推荐