ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Spring Boot + JDBC多数据源配置实战:从双JdbcTemplate到动态路由

2026/9/8 8:13:15 拓冰建站 浏览量
Spring Boot + JDBC多数据源配置实战:从双JdbcTemplate到动态路由 简介Spring Boot与JdbcTemplate结合实现多数据源管理是一份面向Java后端开发者的实用工程示例。资源围绕主、从两个数据源的创建与使用展开演示了通过DataSourceBuilder构建Bean、在application配置中绑定不同prefix参数并利用Qualifier分别注入primary与secondary的JdbcTemplate同时点出Spring Data JPA处理多数据源的扩展思路适合需要连接多个数据库或希望规范数据访问分层的读者。压缩包共116个文件以76个XML配置、16个Java源码、15个Class编译文件及少量properties等为主整体仅66KB结构紧凑便于对照学习。已有303人学习浏览。从源码中可快速掌握多数据源配置的完整骨架DataSourceConfig集中定义数据源与JdbcTemplateDemoApplication提供可直接运行入口Course/User模块按Dao、Service、Controller分层展示实际调用方式对理解自动配置原理及大型项目数据库拆分均具参考价值可直接作为多数据源开发的实战模板。 接手过一个老系统改造业务方上来就提需求报表库和业务库要分开读写要分流后续可能还要再接一个第三方库。说白了就是Spring Boot项目里配多个数据源走JDBC这一层把连接管好。那时候网上搜“springboot jdbc 多数据源”文章倒是不少但要么讲MyBatis要么讲JPA真正把JDBC多数据源从配置到实战说透的没几篇。这篇文章就把我从零配置到上线排错的全过程捋一遍讲清楚方案怎么选、配置怎么写、切换怎么做、坑怎么躲适合正在做多库拆分、读写分离或者接外部数据源的后端开发参考。1. 多数据源这件事关键在思路拆解1.1 什么场景下真的需要多数据源多数据源不是炫技通常来自几种真实需求。一种是业务库和报表库分离业务写入主库报表统计走独立库避免大查询拖垮线上业务另一种是读写分离主库负责写从库负责读这是最常见的双数据源布局还有一种是企业内部系统需要同时访问多个既有的业务库比如订单库、用户库、日志库各自独立部署应用层做数据聚合。在这些场景里Spring Boot默认的单数据源自动配置就不够用了。默认情况下spring.datasource.*只配一个DataSourceJdbcTemplate也好、MyBatis也好绑定的都是这个默认数据源。一旦涉及第二个库就会发现连接串、用户名、密码、驱动全乱套于是必须手动接管数据源配置让应用能同时持有多个DataSource实例并且在使用时能明确指定“这次查哪个库”。1.2 方案选型对比不盲从先算账实现多数据源的常见方案有几种我列一下当时对比的思路。方案一直接创建多个JdbcTemplate Bean。简单直接代码里注入哪个JdbcTemplate就用哪个库场景固定、数据源不常切换时最省事。缺点是如果库的数量多Bean也跟着多管理起来略显繁琐。方案二MyBatis / MyBatis-Plus 多数据源框架。社区里成熟方案很多动态数据源切换、注解控制、读写分离都封装好了适合业务复杂的场景。但它的代价是引入了框架依赖而且在多数据源事务、批处理操作上依然有不少暗坑后面会讲到。方案三AbstractRoutingDataSource动态路由。自己实现一个路由数据源基于ThreadLocal在运行时动态决定走哪个数据源。灵活度高对业务代码几乎无侵入适合有动态切换需求的场景比如按租户分库。缺点是需要自己维护路由上下文和清理逻辑。我当时的需求是报表库和业务库固定分离所以最终选了“方案一为主体方案三做补充”的组合固定的双数据源用两个JdbcTemplate后续要动态切换的地方再单独抽象动态路由。这个思路对大多数中后台项目都适用既不会过度设计又保留了扩展空间。2. 前期准备与配置要点2.1 项目依赖与版本选择如果你准备参照这篇文章动手先确认基础环境。我这里用的是Spring Boot 2.7.xJDK 1.8数据源连接池是Spring Boot默认的HikariCP。依赖上只需要引入JDBC相关的starter和实际使用的数据库驱动。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-jdbc/artifactId /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency一个提醒Spring Boot 2.7.x默认管理的MySQL驱动是8.0.x连接串和驱动类名要按新版的来com.mysql.cj.jdbc.Driver别再用老的com.mysql.jdbc.Driver。如果项目用的数据库是PostgreSQL、SQL Server或者国产数据库驱动依赖替换一下即可核心的DataSource配置逻辑不变。版本这方面我踩过一次教训有同事直接用Spring Boot 3.x配合JDK 8开发结果启动报错因为Spring Boot 3基于Jakarta EE要求JDK 17起步而且javax.*包要改成jakarta.*。如果你的团队对JDK版本没那么激进乖乖用2.7.x最稳搜资料的时候也几乎不会遇到兼容性障碍。2.2 application.yml多数据源配置配置文件的思路是绕过Spring Boot的自动配置前缀把多个数据源写在自定义的prefix下。我习惯用spring.datasource.primary和spring.datasource.secondary区分主从或业务库和报表库。spring: datasource: primary: jdbc-url: jdbc:mysql://localhost:3306/business_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: root123 driver-class-name: com.mysql.cj.jdbc.Driver secondary: jdbc-url: jdbc:mysql://localhost:3306/report_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: report_user password: report123 driver-class-name: com.mysql.cj.jdbc.Driver注意一个细节自定义数据源如果直接配url而不是jdbc-url在部分版本的DataSourceBuilder下会识别不到所以我一律写jdbc-url避免低级问题。数据库连接串务必带上serverTimezone尤其MySQL 8以上否则会有时区相关的报错。2.3 为什么不能用默认的DataSource自动配置默认情况下Spring Boot会读取spring.datasource.url并自动创建一个DataSource。一旦你自定义了多个DataSource的Bean必须想办法“屏蔽”自动配置否则会出现DataSource实例冲突或者JdbcTemplate被自动配置强行绑定到其中某一个上面。处理办法有两种。最优雅的是在启动类上排除DataSourceAutoConfigurationSpringBootApplication(exclude {DataSourceAutoConfiguration.class}) public class MultiDataSourceApplication { public static void main(String[] args) { SpringApplication.run(MultiDataSourceApplication.class, args); } }另一种是只自定义DataSource Bean但不排除自动配置这会让你陷入命名冲突的泥潭。真没必要折腾直接在启动类上加排除最省心。我刚开始就是没加排除结果JdbcTemplate自动注入到了默认的数据源上怎么指定都没用排查了一个下午。3. 核心实现配置类与JdbcTemplate3.1 数据源配置类编写多数据源的核心就是手写配置类把两个DataSource的Bean都交给Spring管理。看代码Configuration public class DataSourceConfig { Bean(name primaryDataSource) Primary ConfigurationProperties(prefix spring.datasource.primary) public DataSource primaryDataSource() { return DataSourceBuilder.create().build(); } Bean(name secondaryDataSource) ConfigurationProperties(prefix spring.datasource.secondary) public DataSource secondaryDataSource() { return DataSourceBuilder.create().build(); } }Primary必须加在主数据源上。道理很简单Spring在注入DataSource类型的对象时如果遇到多个候选Bean会优先选择标记Primary的那个。如果不加启动时会直接报NoUniqueBeanDefinitionException。DataSourceBuilder.create().build()会依据配置里的driver-class-name和jdbc-url自动匹配对应的连接池类型如果什么都不指定Spring Boot默认用HikariCP。这里不需要手动new DruidDataSource或new HikariDataSource让Builder去做代码更简洁。3.2 JdbcTemplate Bean的创建与使用有了DataSource下一步给每个数据源都配一个JdbcTemplate。我希望代码里能明确指示“当前用的是哪个JdbcTemplate”所以在使用的地方通过Qualifier指定。Configuration public class JdbcTemplateConfig { Bean(name primaryJdbcTemplate) public JdbcTemplate primaryJdbcTemplate(Qualifier(primaryDataSource) DataSource dataSource) { return new JdbcTemplate(dataSource); } Bean(name secondaryJdbcTemplate) public JdbcTemplate secondaryJdbcTemplate(Qualifier(secondaryDataSource) DataSource dataSource) { return new JdbcTemplate(dataSource); } }使用的时候要么用Autowired加Qualifier要么直接构造器注入按名字匹配Service public class ReportService { private final JdbcTemplate primaryJdbcTemplate; private final JdbcTemplate secondaryJdbcTemplate; public ReportService(Qualifier(primaryJdbcTemplate) JdbcTemplate primaryJdbcTemplate, Qualifier(secondaryJdbcTemplate) JdbcTemplate secondaryJdbcTemplate) { this.primaryJdbcTemplate primaryJdbcTemplate; this.secondaryJdbcTemplate secondaryJdbcTemplate; } public void queryBusinessAndReport() { ListMapString, Object businessList primaryJdbcTemplate.queryForList(select * from t_order); ListMapString, Object reportList secondaryJdbcTemplate.queryForList(select * from t_report); // 业务处理... } }这样两个JdbcTemplate各管一个库互不干扰。对绝大多数“一个业务库一个报表库”的场景来说这套配置已经够用了。3.3 动态切换数据源用AbstractRoutingDataSource补扩展位如果后续还要按租户切库或者某些请求需要动态选择数据源继续写固定的Bean就捉襟见肘了。这时候可以补一个动态路由的方案。先创建一个路由数据源类继承AbstractRoutingDataSourcepublic class DynamicDataSource extends AbstractRoutingDataSource { Override protected Object determineCurrentLookupKey() { return DataSourceContextHolder.getDataSource(); } }再写一个ThreadLocal工具类保存当前线程要使用的数据源标识public class DataSourceContextHolder { private static final ThreadLocalString CONTEXT new ThreadLocal(); public static void setDataSource(String dataSource) { CONTEXT.set(dataSource); } public static String getDataSource() { return CONTEXT.get(); } public static void clear() { CONTEXT.remove(); } }最后把DynamicDataSource注册成一个Bean它本身也实现了DataSource接口所以可以作为一个整体被注入使用Bean public DynamicDataSource dynamicDataSource(Qualifier(primaryDataSource) DataSource primaryDataSource, Qualifier(secondaryDataSource) DataSource secondaryDataSource) { DynamicDataSource dynamicDataSource new DynamicDataSource(); MapObject, Object targetDataSources new HashMap(); targetDataSources.put(primary, primaryDataSource); targetDataSources.put(secondary, secondaryDataSource); dynamicDataSource.setTargetDataSources(targetDataSources); dynamicDataSource.setDefaultTargetDataSource(primaryDataSource); return dynamicDataSource; }在需要切换的地方调用DataSourceContextHolder.setDataSource(secondary)搞定后调用clear()清掉上下文防止线程池复用导致串库。之前有同事忘了清测试环境数据一会儿对一会儿错查了半天才发现是ThreadLocal没清理这个细节务必放心里。4. 事务与连接管理的那些坑4.1 跨库事务是真的难别硬来多数据源环境下Transactional不再像单数据源那样“万能”。默认情况下一个事务管理器只绑定一个数据源你在方法上写TransactionalSpring会找PlatformTransactionManager类型的Bean如果项目里配置了多个事务管理器必须显式指定用哪个。Bean(name primaryTransactionManager) public DataSourceTransactionManager primaryTransactionManager(Qualifier(primaryDataSource) DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } Bean(name secondaryTransactionManager) public DataSourceTransactionManager secondaryTransactionManager(Qualifier(secondaryDataSource) DataSource dataSource) { return new DataSourceTransactionManager(dataSource); }使用时就变成Transactional(transactionManager primaryTransactionManager) public void updateBusiness() { primaryJdbcTemplate.update(update t_order set status 1 where id ?, id); }如果你硬要在同一个方法里同时写两个库还要求原子性那问题就复杂了。两个独立的事务管理器管的是两个独立连接任何一个阶段报错另一个库已提交的数据回滚不了。这种情况我有话直说跨库强一致事务别指望Spring注解解决要么引入分布式事务中间件要么从业务设计上规避比如先写主库主库成功后异步同步到从库/报表库再配合对账补偿。老老实实接受“最终一致性”往往比硬追求强一致更明智。4.2 连接池参数要单独调别用默认值多数据源环境下连接池参数不再统一primaryDataSource和secondaryDataSource都应该单独配置。HikariCP的默认配置偏保守如果报表库有大批量聚合查询并发稍高一点就会出现连接等待超时。我一般会在每个自定义数据源的配置里补上Hikari的独立配置spring: datasource: primary: jdbc-url: jdbc:mysql://localhost:3306/business_db?... username: root password: root123 driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000 secondary: jdbc-url: jdbc:mysql://localhost:3306/report_db?... username: report_user password: report123 driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 50 minimum-idle: 10 connection-timeout: 30000 idle-timeout: 600000报表库并发高连接池就给大一点业务库连接宝贵就控制在合理范围。连接池满了之后报错通常是Connection is not available, request timed out after 30000ms看到这个错误第一反应就该去查连接池配置而不是去查代码逻辑。4.3 JDBC连接资源必须手动释放使用JdbcTemplate时框架内部已经帮你做了连接释放问题不大。但如果你手写Connection、PreparedStatement、ResultSet记住一定要在finally块里关闭资源或者直接用try-with-resources语法。我遇到过不止一次某段代码里手动获取连接忘记释放导致连接池耗尽数据库连接数拉满最后整库查询卡死。用JdbcTemplate就能规避大部分这类问题这也是我推荐JDBC场景直接用JdbcTemplate而不是裸写JDBC的原因。5. 常见问题与排查实录5.1 问题速查表问题现象大概率原因解决思路启动报错NoUniqueBeanDefinitionException存在多个DataSource或JdbcTemplate Bean没有指定主次主数据源加Primary注入时用Qualifier明确指定Bean名称报错name jdbc is not bound in this context配置中用了JNDI方式如spring.datasource.jndi-name但容器里根本没有对应JNDI资源改为常规的jdbc-url连接配置别在标准Spring Boot应用里用JNDI报错No suitable driver found for jdbc:mysql://...驱动依赖缺失或驱动类名和连接串不匹配检查pom依赖确认MySQL驱动版本驱动类用com.mysql.cj.jdbc.Driver两个库的数据错乱ThreadLocal未清理或非Primary数据源被自动装配误用动态切换后必须在finally中执行DataSourceContextHolder.clear()连接超时Connection is not available连接池太小SQL慢或连接泄漏调大maximum-pool-size排查慢SQL和泄漏连接使用事务时部分数据回滚不了跨库事务不同事务管理器各自管理各自连接重新设计事务边界或引入分布式事务组件5.2 一个MyBatis-Plus用户的“SaveOrUpdateBatch多数据源”问题热搜词里有一条“mybatis的saveorupdatebatch多数据源的问题”我顺手说一下。有人在使用MyBatis-Plus时调saveOrUpdateBatch在配置了动态数据源的情况下偶尔报错或数据进了错误的库。原因多半是批量操作里数据源切换的时机和事务绑定对不上Spring的事务管理器在事务开启时就确定了连接如果动态数据源切换发生在事务执行中间连接已经持有旧数据源的引用切换就失效了。这类问题的解决思路是把多数据源场景下的批量写入拆成两部分先确定数据源再开启事务不要在一个事务里来回切库。如果必须批处理建议每个库单独走一批不要在同一个循环体里对两个库交替写入。5.3 排查技巧开启Hikari的日志和SQL输出真出问题不要靠猜把SQL和连接获取日志打开问题定位会快很多。在application.yml里加一行logging: level: org.springframework.jdbc.core.JdbcTemplate: DEBUG这样JdbcTemplate执行的SQL参数都会打到日志里可以根据日志确认当前线程用的到底是哪个数据源、哪条SQL。我排查串库问题的时候就是靠这个日志一眼看到业务方法里打印出来的是报表库的SQL才反推回去发现是ThreadLocal没清理。6. 一些实操心得与建议多数据源配置本身不复杂复杂的是使用场景和边界管理。我现在的惯例是固定双库用双JdbcTemplate Bean简单清楚动态场景才上AbstractRoutingDataSource并且把DataSourceContextHolder的清理逻辑封装到切面里统一处理避免项目里散落一地的try-finally代码。另外多数据源一定要和运维对齐连接信息包括账号权限、网络白名单、防火墙策略等。之前有次上线后应用连不上报表库排查很久发现是报表库账号只授权了本机访问应用服务器IP没进白名单。这个问题和代码无关但比代码问题更难排查。提前把环境配置确认好能省掉大把时间。如果你也是刚接手多数据源改造建议先画一张简单的表理清每个数据源对应的库、业务场景、连接池大小、事务边界再动手写代码。配置类的代码就那些最怕的是需求没理清就开写最后代码和业务归属全乱套。希望这篇实战记录能帮你少走一些弯路。本文还有配套的精品资源点击获取