简介:这份PDF资料面向使用SpringBoot与MyBatis的Java后端开发者,聚焦多数据源配置这一常见痛点,尤其适合需要处理主从模式或分库业务、又不想引入JPA或AOP动态切换等复杂方案的中级开发者。资源为单个PDF文件,压缩包约54KB,内容围绕最简多数据源实现思路展开,涵盖pom依赖引入、application.properties中test1与test2双数据源连接配置,以及通过@Configuration、@MapperScan、@Primary等注解逐层注入DataSource、SqlSessionFactory、TransactionManager与SqlSessionTemplate的完整代码示例,并说明dao层与xml按库分目录组织的做法。目前已有2357人学习下载,可作为快速搭建多数据源骨架、理解主库指定与分库扫描机制的参考,帮助读者减少配置试错时间,把握多数据源方案的核心脉络。
1. 多数据源这件事,最怕一上来就上分布式事务
同一个 Spring Boot 服务里连两个库,绝大多数团队的真实诉求其实很朴素:主库写业务,从库或历史库读报表;或者一个库放订单,一个库放日志。可一旦搜“多数据源”,跳出来的方案动辄就是动态路由、AOP 切面、分布式事务、Seata 全家桶,照着配三天,启动报一堆DataSource循环依赖,最后连单库查询都跑不通了。我见过太多项目在这上面翻车,根因不是技术难,而是把“多数据源”和“多数据源+读写分离+分库分表+分布式事务”混成了一件事。这篇笔记只解决最简路径:用 Spring Boot 原生能力 + MyBatis,把两个独立数据源接进同一个应用,各自有独立的SqlSessionFactory和Mapper扫描路径,互不干扰,不引入任何中间件。适合手上有两个库要同时读写、又不想把架构搞复杂的后端同学。下面按“先立住概念,再动手复现,最后讲坑”的顺序推。
2. 先想清楚:多数据源到底在 Spring 里怎么落地
2.1 单数据源时 Spring Boot 替你做了什么
平时我们只写一个spring.datasource.url,应用就能跑,是因为 Spring Boot 的DataSourceAutoConfiguration在背后做了几件事:读取配置、创建DataSource、把它交给MybatisAutoConfiguration,后者再创建SqlSessionFactory和SqlSessionTemplate,最后@MapperScan把 Mapper 接口注册成 Bean。整条链路是自动装配的,你感知不到。
多数据源的本质,就是这条自动装配链路要手动复制一份。两个库意味着两个DataSource、两个SqlSessionFactory、两个SqlSessionTemplate、两套@MapperScan路径。Spring 不会自动帮你区分哪个 Mapper 该用哪个工厂,所以必须手动指定。理解这一点,后面所有配置都是顺理成章的。
2.2 为什么不用动态数据源路由
动态数据源(AbstractRoutingDataSource)的思路是:只保留一个SqlSessionFactory,在运行时根据ThreadLocal里的 key 切换DataSource。它适合读写分离这种“同一个 Mapper 有时走主有时走从”的场景。但它有两个代价:一是切面要侵入业务代码或靠注解,二是事务边界容易出问题——同一个事务里切了库,事务就不完整了。
如果你的两个库是业务隔离的(订单 Mapper 永远走订单库,日志 Mapper 永远走日志库),用固定多数据源更简单、更稳,没有 ThreadLocal 泄漏风险,也没有切面顺序的玄学问题。选型判断标准就一句话:Mapper 和库是不是一一对应。是,就用本文方案;不是,再考虑动态路由。
2.3 最小方案的四个组成部分
一个能跑的最小多数据源方案,需要四样东西:
| 组成部分 | 作用 | 数量 |
|---|---|---|
| DataSource | 连接池实例,绑定一个库 | 每个库一个 |
| SqlSessionFactory | MyBatis 核心工厂,绑定一个 DataSource | 每个库一个 |
| SqlSessionTemplate | 线程安全的会话模板 | 每个库一个 |
| MapperScan | 指定该库的 Mapper 接口包路径 | 每个库一套 |
关键约束:@MapperScan的sqlSessionTemplateRef必须指向对应的模板,否则 Mapper 会绑错工厂,查询跑到另一个库去,这种错误往往在运行时才暴露,排查起来很费时间。
3. 动手:两个库接进一个 Spring Boot 应用
3.1 依赖与配置:把两个库的坐标写清楚
先看依赖。核心是 Spring Boot 的 MyBatis starter 和数据库驱动,两个库如果类型不同(比如一个 MySQL 一个 PostgreSQL),驱动都要引。
<!-- pom.xml 关键依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <!-- 两个库的驱动,按实际数据库类型选 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> </dependency>配置文件里,两个数据源用不同的前缀区分。注意这里不要再用spring.datasource这个默认前缀,否则自动装配会来抢,导致冲突。
# application.yml primary: datasource: url: jdbc:mysql://127.0.0.1:3306/order_db?useSSL=false&serverTimezone=Asia/Shanghai username: order_user password: order_pass driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 10 minimum-idle: 2 secondary: datasource: url: jdbc:mysql://127.0.0.1:3306/log_db?useSSL=false&serverTimezone=Asia/Shanghai username: log_user password: log_pass driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 5 minimum-idle: 1参数说明:maximum-pool-size按库的实际压力设,日志库通常读少写多,池子可以小一点;minimum-idle不要设成 0,否则每次请求都要新建连接,延迟会抖。两个库的serverTimezone必须显式指定,否则 MySQL 8 驱动在某些时区下会报时间转换异常,这是血泪经验。
3.2 主数据源配置类:@Primary 不能漏
主数据源要加@Primary,因为 Spring 在按类型注入DataSource时,如果有多个候选,没有主候选会直接启动失败。
@Configuration @MapperScan( basePackages = "com.example.order.mapper", sqlSessionTemplateRef = "primarySqlSessionTemplate" ) public class PrimaryDataSourceConfig { @Bean(name = "primaryDataSource") @Primary @ConfigurationProperties(prefix = "primary.datasource") public DataSource primaryDataSource() { // 用 HikariDataSource,配置由 @ConfigurationProperties 自动绑定 return DataSourceBuilder.create().type(HikariDataSource.class).build(); } @Bean(name = "primarySqlSessionFactory") @Primary public SqlSessionFactory primarySqlSessionFactory( @Qualifier("primaryDataSource") DataSource dataSource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); // 指定该库的 XML 映射文件位置,没有 XML 可省略 bean.setMapperLocations( new PathMatchingResourcePatternResolver() .getResources("classpath:mapper/order/*.xml")); return bean.getObject(); } @Bean(name = "primarySqlSessionTemplate") @Primary public SqlSessionTemplate primarySqlSessionTemplate( @Qualifier("primarySqlSessionFactory") SqlSessionFactory factory) { return new SqlSessionTemplate(factory); } }逻辑说明:@ConfigurationProperties(prefix = "primary.datasource")把 yaml 里的配置自动绑定到DataSource上,省去手写setUrl那一堆。@MapperScan的sqlSessionTemplateRef是关键,它告诉 Spring 这个包下的 Mapper 用哪个模板。@Primary加在三个 Bean 上,保证默认注入时不会歧义。
3.3 从数据源配置类:去掉 @Primary,改名字
从数据源配置和主数据源几乎一样,区别只有三处:Bean 名字不同、@MapperScan路径不同、不加@Primary。
@Configuration @MapperScan( basePackages = "com.example.log.mapper", sqlSessionTemplateRef = "secondarySqlSessionTemplate" ) public class SecondaryDataSourceConfig { @Bean(name = "secondaryDataSource") @ConfigurationProperties(prefix = "secondary.datasource") public DataSource secondaryDataSource() { return DataSourceBuilder.create().type(HikariDataSource.class).build(); } @Bean(name = "secondarySqlSessionFactory") public SqlSessionFactory secondarySqlSessionFactory( @Qualifier("secondaryDataSource") DataSource dataSource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); bean.setMapperLocations( new PathMatchingResourcePatternResolver() .getResources("classpath:mapper/log/*.xml")); return bean.getObject(); } @Bean(name = "secondarySqlSessionTemplate") public SqlSessionTemplate secondarySqlSessionTemplate( @Qualifier("secondarySqlSessionFactory") SqlSessionFactory factory) { return new SqlSessionTemplate(factory); } }注意@Qualifier必须写,因为容器里现在有两个DataSource、两个SqlSessionFactory,不指定名字 Spring 不知道注入哪个。Mapper 接口分别放在com.example.order.mapper和com.example.log.mapper两个包下,物理隔离,避免扫描串包。
3.4 验证:写一个双库查询的测试
配置完别急着写业务,先验证两个库都能通。写一个简单的测试类,分别调两个 Mapper。
@SpringBootTest class MultiDataSourceTest { @Autowired private OrderMapper orderMapper; @Autowired private LogMapper logMapper; @Test void testBothDataSources() { // 查订单库 Long orderCount = orderMapper.countAll(); System.out.println("order_db count = " + orderCount); // 查日志库 Long logCount = logMapper.countAll(); System.out.println("log_db count = " + logCount); assertNotNull(orderCount); assertNotNull(logCount); } }如果两个 count 都能打出来,说明数据源绑定正确。如果其中一个报Table doesn't exist,大概率是 Mapper 绑错了库——去检查@MapperScan的sqlSessionTemplateRef有没有写对。这个测试是后悔药,早写早省心。
4. 避坑:多数据源最容易翻车的五个地方
4.1 启动报 DataSource 循环依赖
现象:启动时抛BeanCurrentlyInCreationException,提示dataSource和sqlSessionFactory互相依赖。原因:某个配置类里既注入了DataSource又注入了SqlSessionFactory,而工厂又依赖数据源,形成环。解决:配置类里只通过方法参数注入,不要在字段上@Autowired两个互相依赖的 Bean;或者用@Lazy打破环。我一般直接改成方法参数注入,干净。
4.2 Mapper 扫描串包,查询跑到错误的库
现象:订单查询返回了日志库的表数据,或者报“表不存在”。原因:两个@MapperScan的basePackages有重叠,或者sqlSessionTemplateRef写成了对方的模板。解决:两个 Mapper 包路径必须完全隔离,不要用com.example这种父包同时扫;sqlSessionTemplateRef的值和 Bean 名字逐字核对。这个坑最隐蔽,因为编译期不报错。
4.3 事务注解失效,跨库操作不回滚
现象:@Transactional标注的方法里操作了两个库,第一个库失败后第二个库的数据没回滚。原因:Spring 的DataSourceTransactionManager默认只管理一个数据源,多数据源下需要为每个库配独立的事务管理器,且@Transactional要指定transactionManager。解决:给每个数据源配一个DataSourceTransactionManager,在注解里显式指定。但更实际的做法是:跨库操作不要放在同一个事务里,用业务补偿代替,因为分布式事务的成本远高于这个最简方案的目标。
4.4 连接池参数照抄,日志库把连接占满
现象:日志库偶尔报Connection is not available, request timed out。原因:两个库共用了同一套连接池参数,日志库写入频繁但池子设得和订单库一样大,或者反过来订单库池子太小。解决:每个库独立配hikari参数,按实际 QPS 估算。经验值:maximum-pool-size约等于峰值并发数,connection-timeout设 3000ms 左右,别用默认的 30 秒,否则故障时线程全堵死。
4.5 单元测试里 @Transactional 回滚只回滚主库
现象:测试方法加了@Transactional,跑完发现从库的数据没回滚,污染了测试库。原因:测试的事务管理器默认是主数据源的,从库操作不在同一个事务里。解决:测试类上指定@Transactional(transactionManager = "primaryTransactionManager")只能管主库;要清理从库,要么手动删,要么用@Sql脚本在测试后执行清理。最省事的办法是测试库用独立 schema,跑完直接 truncate。
5. 进阶:把配置抽成模板,加一个动态开关
两个库手写两套配置还能忍,如果以后加到三个、四个库,复制粘贴就变成维护灾难。我一般会把配置类抽成一个抽象基类,子类只提供前缀和 Mapper 包路径。
public abstract class AbstractDataSourceConfig { protected SqlSessionFactory buildSqlSessionFactory( DataSource dataSource, String mapperLocation) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); bean.setMapperLocations( new PathMatchingResourcePatternResolver().getResources(mapperLocation)); return bean.getObject(); } protected DataSource buildDataSource() { return DataSourceBuilder.create().type(HikariDataSource.class).build(); } }子类继承后只写@Bean方法和@MapperScan,重复代码能砍掉一半。另一个实用技巧是加一个@ConditionalOnProperty开关,让从数据源在本地开发时可以关闭,避免本地没起日志库导致启动失败。
@Bean(name = "secondaryDataSource") @ConditionalOnProperty(name = "secondary.datasource.enabled", havingValue = "true") @ConfigurationProperties(prefix = "secondary.datasource") public DataSource secondaryDataSource() { return DataSourceBuilder.create().type(HikariDataSource.class).build(); }本地application-local.yml里把secondary.datasource.enabled设成false,从数据源相关的 Bean 全部不创建,启动就轻了。但要注意,如果 Mapper 扫描还在,会报找不到模板,所以@MapperScan也要加同样的条件,或者把从库 Mapper 单独放一个配置类,整体加条件。
验证多数据源是否真的生效,除了前面的双库查询测试,还有一个更直接的办法:在SqlSessionFactory创建后打印它的dataSource的jdbcUrl。我习惯在配置类里加一行日志,启动时看一眼两个工厂分别连的哪个库,比事后排查省事得多。这个习惯帮我省下过至少两次通宵定位的时间。多数据源本身不复杂,复杂的是边界没划清就动手,希望帮到你。
本文还有配套的精品资源,点击获取