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。
/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");
}
}
3.3 动态数据源路由——让报表自己选数据库
前面讲的多数据源是“硬编码”的——哪个报表用哪个库,写死在代码里。但报表引擎的场景更灵活:用户创建一张报表时,可能指定数据源A,也可能指定数据源B,甚至运行时才决定。
这时候就需要动态路由了。说白了,就是根据某个标识(比如报表ID、租户ID),在运行时动态切换到对应的数据源。
我实现动态路由的思路是这样的:
- 定义一个
DynamicDataSource类,继承Spring的AbstractRoutingDataSource。 - 重写
determineCurrentLookupKey()方法,返回当前线程要用的数据源key。 - 用
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动态生成报表内容。到时候见!