3、报表数据源配置:JDBC连接池配置、多数据源切换(MySQL+SQL Server)、动态数据源路由实现

好,咱们接着聊报表引擎的核心——数据源配置。

说实话,报表引擎说白了就是个“数据搬运工”。你前端画得再漂亮,如果数据拿不到、拿得慢、拿错了,那一切都是白搭。我早期做报表系统时,就吃过这个亏——数据源配得随意,结果报表加载慢得像蜗牛,还被业务部门投诉过好几次。

今天这一节,咱们把数据源这块彻底讲透。我会从连接池配置讲起,再到多数据源切换,最后是动态路由。嗯,一步步来。

3.1 JDBC连接池配置——别让数据库成为瓶颈

先问个问题:为什么不用最原始的JDBC直连?每次请求都创建连接、用完再关?

我告诉你,这样做在开发环境跑两个接口还行,一旦上了生产,并发一上来,数据库连接数瞬间被打满,系统直接瘫痪。我在一个老项目里见过这种“裸奔”写法,结果DBA半夜打电话骂人。

所以,连接池是必须的。它维护了一组“活着的”数据库连接,你用的时候借一个,用完还回去,省去了反复创建销毁的开销。

我个人习惯用HikariCP,Spring Boot 2.x以后默认就是它。为什么?快,稳,而且配置简单。来看一段典型的配置:

# application.yml
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/report_db?useSSL=false&serverTimezone=Asia/Shanghai
    username: report_user
    password: report_pass
    driver-class-name: com.mysql.cj.jdbc.Driver
    hikari:
      pool-name: ReportPool
      maximum-pool-size: 20
      minimum-idle: 5
      idle-timeout: 300000
      connection-timeout: 20000
      max-lifetime: 1200000

这里有几个参数我重点说一下:

  • maximum-pool-size:最大连接数。不是越大越好!我见过有人设成200,结果数据库扛不住,反而更慢。一般20~50够用,具体压测决定。
  • minimum-idle:最小空闲连接。建议设成5左右,别让连接池在低峰期也空着。
  • connection-timeout:等待连接的超时时间。如果所有连接都在忙,新请求最多等20秒,超时就报错。这个值别设太大,否则用户会等到崩溃。
  • max-lifetime:连接的最大存活时间。MySQL默认8小时断开空闲连接,所以这个值建议小于数据库的wait_timeout。
我的小技巧: 生产环境建议开启HikariCP的监控指标,通过/actuator/health可以实时查看连接池状态。我曾经靠这个发现了一个连接泄漏的bug——某个报表查询忘了关闭ResultSet,导致连接数只增不减。

3.2 多数据源切换——MySQL + SQL Server 混搭

实际项目中,很少有公司只用一种数据库。我遇到过的情况是:业务数据在MySQL里,财务数据在SQL Server里,报表引擎需要同时从两边取数。

这时候,你就得配多个数据源。Spring Boot里怎么做?我一般用@Primary@Qualifier来区分。

先看配置:

spring:
  datasource:
    mysql:
      url: jdbc:mysql://192.168.1.100:3306/biz_db
      username: biz_user
      password: biz_pass
      driver-class-name: com.mysql.cj.jdbc.Driver
    sqlserver:
      url: jdbc:sqlserver://192.168.1.200:1433;databaseName=finance_db
      username: finance_user
      password: finance_pass
      driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver

然后定义两个数据源Bean:

@Configuration
public class DataSourceConfig {

    @Primary
    @Bean(name = "mysqlDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.mysql")
    public DataSource mysqlDataSource() {
        return DataSourceBuilder.create().build();
    }

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

使用时,通过@Qualifier指定:

@Service
public class ReportService {

    @Qualifier("mysqlDataSource")
    private DataSource mysqlDataSource;

    @Qualifier("sqlServerDataSource")
    private DataSource sqlServerDataSource;

    public List<Map<String, Object>> getBizData() {
        // 从MySQL取业务数据
        return new JdbcTemplate(mysqlDataSource).queryForList("SELECT * FROM orders");
    }

    public List<Map<String, Object>> getFinanceData() {
        // 从SQL Server取财务数据
        return new JdbcTemplate(sqlServerDataSource).queryForList("SELECT * FROM accounts");
    }
}
注意: 多数据源时,事务管理会变得复杂。如果同一个方法里需要操作两个数据源,建议使用分布式事务(如Seata),或者干脆拆成两个独立的事务。我曾经因为没注意这个,导致MySQL写成功了,SQL Server写失败了,数据对不上,排查了一整天。

3.3 动态数据源路由——让报表自己选数据库

前面讲的多数据源是“硬编码”的——哪个报表用哪个库,写死在代码里。但报表引擎的场景更灵活:用户创建一张报表时,可能指定数据源A,也可能指定数据源B,甚至运行时才决定。

这时候就需要动态路由了。说白了,就是根据某个标识(比如报表ID、租户ID),在运行时动态切换到对应的数据源。

我实现动态路由的思路是这样的:

  1. 定义一个DynamicDataSource类,继承Spring的AbstractRoutingDataSource
  2. 重写determineCurrentLookupKey()方法,返回当前线程要用的数据源key。
  3. ThreadLocal保存当前线程的数据源key,在请求开始时设置,结束时清除。

来看代码:

public class DynamicDataSource extends AbstractRoutingDataSource {

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

    @Override
    protected Object determineCurrentLookupKey() {
        return CONTEXT_HOLDER.get();
    }

    public static void setDataSource(String dataSourceKey) {
        CONTEXT_HOLDER.set(dataSourceKey);
    }

    public static void clearDataSource() {
        CONTEXT_HOLDER.remove();
    }
}

然后在配置类里注册这个动态数据源:

@Configuration
public class DynamicDataSourceConfig {

    @Bean
    @Primary
    public DataSource dataSource(
            @Qualifier("mysqlDataSource") DataSource mysql,
            @Qualifier("sqlServerDataSource") DataSource sqlServer) {

        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("mysql", mysql);
        targetDataSources.put("sqlserver", sqlServer);

        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        dynamicDataSource.setDefaultTargetDataSource(mysql); // 默认用MySQL
        dynamicDataSource.setTargetDataSources(targetDataSources);
        return dynamicDataSource;
    }
}

使用时,在报表查询前设置数据源:

public List<Map<String, Object>> executeReport(String reportId) {
    // 从报表配置中获取数据源标识
    String dsKey = reportConfigService.getDataSourceKey(reportId);
    DynamicDataSource.setDataSource(dsKey);
    try {
        // 执行查询,此时会自动路由到对应的数据源
        return jdbcTemplate.queryForList("SELECT * FROM report_data WHERE report_id = ?", reportId);
    } finally {
        // 一定要清理,否则线程复用时会出问题
        DynamicDataSource.clearDataSource();
    }
}
核心要点: 动态路由的关键在于ThreadLocal。每个请求线程独立维护自己的数据源选择,互不干扰。但千万记得在finally块里清理,否则线程池复用时会读到脏数据。我早期就踩过这个坑——一个报表查完没清理,下一个请求莫名其妙用了上一个的数据源,数据全乱了。

3.4 避坑指南——我踩过的那些坑

讲到这里,我把这些年积累的几个血泪教训分享给你:

  • 连接池泄漏:我曾经有个报表,查询逻辑里用了DataSourceUtils.getConnection(),但忘了调用close()。结果连接池被慢慢耗尽,系统每隔两天就要重启一次。后来用HikariCP的监控才发现问题。
  • 事务与数据源不匹配:动态路由时,如果事务管理器绑定了默认数据源,那切换数据源后事务可能失效。我的做法是:每个数据源配独立的事务管理器,或者干脆不用声明式事务,手动控制。
  • SQL Server的驱动坑:SQL Server的JDBC驱动有个“特性”——默认不启用TCP连接复用。如果你频繁创建连接,性能会非常差。记得在连接URL里加上encrypt=false;trustServerCertificate=true,并且用连接池管理。
  • 数据源密码加密:别把密码明文写在配置文件里!我见过有人把生产库密码直接提交到Git仓库,后果可想而知。建议用Jasypt或Spring Cloud Config做加密。
我的建议: 如果你刚开始做报表引擎,先别急着上动态路由。先把单数据源的连接池配好,再扩展到多数据源,最后才考虑动态路由。一步一个脚印,稳扎稳打。

好了,数据源配置这块就讲到这里。下一节我们会聊报表模板引擎——如何用FreeMarker和Velocity动态生成报表内容。到时候见!