1. 为什么需要双数据源架构1.1 从一个真实场景说起很多项目起步阶段都是单一数据库打天下MySQL 用得挺顺手增删改查、事务管理、连接池配置都轻车熟路。但业务一旦往深水区走情况就变了。我接手过一个数据报表类的项目核心业务数据放在 MySQL 里但有一部分历史数据和对账数据存在 SQL Server 中原因是上游合作方只提供 SQL Server 的接口而且数据量不小每天都有增量同步的需求。这时候就面临一个选择要么在应用层做数据搬运把 SQL Server 的数据定时同步到 MySQL要么让应用直接同时连接两个数据库按需读写。前者增加了数据延迟和同步逻辑的复杂度后者则需要在 SpringBoot 中配置双数据源。我最终选了后者。原因很直接业务要求实时查询对账数据同步方案带来的延迟不可接受。而且双数据源配置一旦跑通后续扩展第三个数据源也只是复制粘贴的事。1.2 双数据源到底解决了什么问题简单来说双数据源让一个 SpringBoot 应用同时管理两个独立的数据库连接。每个数据源有自己的连接池、自己的事务管理器、自己的 MyBatis 或 JPA 配置。应用可以根据业务逻辑自由选择从哪个库读、往哪个库写。这跟分库分表不是一回事。分库分表是同一个数据库类型下的水平拆分而双数据源往往是异构数据库——MySQL 和 SQL Server 的驱动不同、方言不同、连接参数不同甚至事务隔离级别的默认行为都有差异。注意双数据源不等于分布式事务。如果你需要跨库的强一致性事务那是另一个层面的问题需要引入分布式事务框架。大多数双数据源场景下两个库的业务是相对独立的各自管好自己的事务就行。1.3 适合哪些人参考这篇内容适合已经用过 SpringBoot 和 MyBatis、但对多数据源配置还不太熟悉的后端开发。如果你正在做数据迁移、报表系统、多租户架构或者单纯想搞清楚Configuration和MapperScan在双数据源下怎么配合那接下来的内容应该能帮你少走弯路。我会从依赖引入开始一步步把 MySQL 和 SQL Server 双数据源的配置拆开讲清楚包括连接池选型、事务管理器配置、MyBatis 映射扫描、以及实际开发中踩过的坑。2. 整体方案设计与技术选型2.1 核心思路手动配置替代自动装配SpringBoot 的自动装配在单数据源时非常省心spring.datasource.url一配DataSource 就自动注入到容器里了。但双数据源场景下自动装配反而成了障碍——它不知道该把哪个数据源作为主数据源也不知道Transactional该用哪个事务管理器。所以核心思路是放弃自动装配手动声明两个 DataSource Bean分别绑定各自的配置属性再为每个数据源单独配置 SqlSessionFactory 和 TransactionManager。这样做的好处是控制权完全在自己手里。哪个是主数据源、哪个是从数据源、哪个 Mapper 扫描哪个包全部显式声明不会出现“以为配好了结果跑起来发现连的是另一个库”这种问题。2.2 连接池选型HikariCP 还是 Druid连接池的选择直接影响应用的稳定性和监控能力。我对比过几种常见方案连接池优势劣势适用场景HikariCP性能极高SpringBoot 默认监控功能弱追求极致性能Druid监控完善SQL 防火墙配置稍复杂需要 SQL 审计Tomcat JDBC稳定成熟性能一般传统项目我最终选了 HikariCP原因是这个项目对性能敏感而且已经有独立的 APM 监控不需要 Druid 自带的监控面板。如果你的项目需要 SQL 级别的审计和慢查询统计Druid 会更合适。提示无论选哪个连接池两个数据源的连接池要独立配置。不要试图共用一个连接池配置对象否则改了一个另一个也跟着变。2.3 包结构规划按数据源分包双数据源最容易出问题的地方是 Mapper 扫描。如果两个数据源的 Mapper 混在同一个包下MapperScan就会把两个数据源的 Mapper 都扫进去导致某个 Mapper 被错误地绑定到另一个数据源的 SqlSessionFactory 上。我的做法是按数据源分包com.example.project ├── mapper │ ├── mysql │ │ ├── UserMapper.java │ │ └── OrderMapper.java │ └── sqlserver │ ├── ReportMapper.java │ └── ReconciliationMapper.java ├── entity │ ├── mysql │ └── sqlserver ├── service │ ├── mysql │ └── sqlserver └── config ├── MysqlDataSourceConfig.java └── SqlServerDataSourceConfig.java这样在配置MapperScan时直接指定basePackages就能精确控制扫描范围。实体类也分开避免 MyBatis 的 typeAlias 冲突。2.4 事务管理器的隔离策略每个数据源配一个DataSourceTransactionManager分别指定各自的数据源。在 Service 层使用Transactional时通过transactionManager属性指定用哪个事务管理器。这里有个容易忽略的点默认的Transactional会找名为transactionManager的 Bean。如果你两个事务管理器一个叫mysqlTransactionManager一个叫sqlServerTransactionManager那不加限定符的Transactional会直接报错找不到 Bean。解决办法是要么把主数据源的事务管理器命名为transactionManager要么在每个Transactional上显式指定。我选择显式指定虽然多写几个字但代码可读性更好后来的人一看就知道这个事务走的是哪个库。3. 核心配置细节与实操要点3.1 依赖引入的注意事项Maven 依赖需要同时引入 MySQL 和 SQL Server 的驱动。这里有个版本兼容性问题SQL Server 的 JDBC 驱动版本要和 JDK 版本匹配。JDK 8 用mssql-jdbc的 7.x 或 8.x 版本比较稳JDK 11 以上可以用 9.x 或 10.x。dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdcom.microsoft.sqlserver/groupId artifactIdmssql-jdbc/artifactId version9.4.1.jre8/version /dependency dependency groupIdcom.zaxxer/groupId artifactIdHikariCP/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency注意mybatis-spring-boot-starter会自带一个 DataSource 的自动装配如果你手动配置了 DataSource Bean需要在启动类上排除DataSourceAutoConfiguration否则会报“多个 DataSource Bean”的冲突。3.2 配置文件的分组隔离application.yml中两个数据源的配置要分开写用不同的前缀区分spring: datasource: mysql: jdbc-url: jdbc:mysql://localhost:3306/business_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: mysql_pass driver-class-name: com.mysql.cj.jdbc.Driver maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 sqlserver: jdbc-url: jdbc:sqlserver://localhost:1433;DatabaseNamereport_db;encryptfalse username: sa password: sqlserver_pass driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver maximum-pool-size: 10 minimum-idle: 3 connection-timeout: 30000这里有几个细节值得展开第一MySQL 的 URL 参数。serverTimezone必须指定否则 MySQL 8 驱动会报时区错误。useUnicode和characterEncoding保证中文不乱码。第二SQL Server 的 URL 格式。它用的是分号分隔的参数不是。encryptfalse在开发环境可以关掉加密生产环境建议开启并配置证书。第三连接池大小。MySQL 是主库读写频繁给 20 个连接SQL Server 是报表查询并发不高给 10 个就够。连接池不是越大越好每个连接都占内存而且数据库端也有最大连接数限制。3.3 数据源配置类的编写MySQL 数据源配置类Configuration MapperScan( basePackages com.example.project.mapper.mysql, sqlSessionFactoryRef mysqlSqlSessionFactory ) public class MysqlDataSourceConfig { Primary Bean(name mysqlDataSource) ConfigurationProperties(prefix spring.datasource.mysql) public DataSource mysqlDataSource() { return DataSourceBuilder.create().type(HikariDataSource.class).build(); } Primary Bean(name mysqlSqlSessionFactory) public SqlSessionFactory mysqlSqlSessionFactory( Qualifier(mysqlDataSource) DataSource dataSource) throws Exception { SqlSessionFactoryBean bean new SqlSessionFactoryBean(); bean.setDataSource(dataSource); bean.setMapperLocations( new PathMatchingResourcePatternResolver() .getResources(classpath:mapper/mysql/*.xml)); return bean.getObject(); } Primary Bean(name mysqlTransactionManager) public DataSourceTransactionManager mysqlTransactionManager( Qualifier(mysqlDataSource) DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } }SQL Server 数据源配置类结构相同只是把Primary去掉包路径和 Bean 名称换成对应的。提示Primary标记的是默认数据源。当有多个同类型 Bean 时Spring 会优先注入Primary的那个。把 MySQL 设为主数据源是因为大部分业务操作走 MySQL这样在不确定用哪个数据源的地方默认走 MySQL 更安全。3.4 MyBatis XML 映射文件的隔离如果 Mapper 接口用注解写 SQL那MapperScan指定包路径就够了。但如果用 XML 写 SQL还需要在SqlSessionFactoryBean中指定mapperLocations。两个数据源的 XML 文件放在不同目录下resources ├── mapper │ ├── mysql │ │ ├── UserMapper.xml │ │ └── OrderMapper.xml │ └── sqlserver │ ├── ReportMapper.xml │ └── ReconciliationMapper.xml这样配置mapperLocations时用classpath:mapper/mysql/*.xml和classpath:mapper/sqlserver/*.xml就能精确匹配。注意如果 XML 文件和 Mapper 接口不在同一个包下MyBatis 默认是找不到的。必须通过mapperLocations显式指定或者把 XML 放在和接口相同的包路径下Maven 需要配置resources插件把src/main/java下的 XML 也打包进去。4. 实操过程与核心环节实现4.1 从零搭建的完整步骤假设你有一个全新的 SpringBoot 项目要加上 MySQL 和 SQL Server 双数据源按下面的顺序操作第一步创建项目骨架。用你习惯的方式生成一个 SpringBoot 项目JDK 版本根据实际情况选。启动类上加上SpringBootApplication(exclude {DataSourceAutoConfiguration.class})排除自动装配。第二步引入依赖。把前面列的 MySQL 驱动、SQL Server 驱动、HikariCP、MyBatis Starter 都加到pom.xml里。注意 SQL Server 驱动的版本要和 JDK 匹配。第三步写配置文件。在application.yml中按spring.datasource.mysql和spring.datasource.sqlserver两个前缀分别配置连接信息。第四步创建配置类。按前面的模板创建MysqlDataSourceConfig和SqlServerDataSourceConfig注意包路径和 Bean 名称要对应。第五步创建 Mapper 和实体类。在mapper.mysql和mapper.sqlserver包下分别创建 Mapper 接口在entity.mysql和entity.sqlserver下创建实体类。第六步写 Service 层。Service 中通过Autowired注入对应的 Mapper需要事务的地方用Transactional(transactionManager mysqlTransactionManager)指定事务管理器。第七步启动测试。写一个简单的 Controller 或单元测试分别调用两个数据源的查询确认都能正常返回数据。4.2 参数计算与连接池调优连接池大小的计算有个经验公式连接数 (核心线程数 × 2) 磁盘队列数。但实际项目中更常用的是根据 QPS 和平均查询耗时来估算。假设 MySQL 的峰值 QPS 是 500平均查询耗时 20ms那么需要的连接数大约是500 × 0.02 10。考虑到突发流量给 20 个连接是合理的。SQL Server 的 QPS 假设是 50平均耗时 100ms需要50 × 0.1 5个连接给 10 个留足余量。connection-timeout设 30 秒意思是获取连接最多等 30 秒超时就抛异常。这个值不要设太大否则请求堆积时会拖垮整个应用。idle-timeout和max-lifetime用默认值就行HikariCP 的默认值已经比较合理。4.3 跨数据源查询的处理方式有些场景需要同时查两个库的数据比如报表要关联 MySQL 的用户信息和 SQL Server 的交易记录。这种情况没法用一条 SQL 搞定我的做法是在 Service 层分别查询然后在内存中做关联。Service public class ReportService { Autowired private UserMapper userMapper; Autowired private ReportMapper reportMapper; public ListReportVO getReport(Long userId) { User user userMapper.selectById(userId); ListTransaction transactions reportMapper.selectByUserId(userId); return transactions.stream() .map(t - convertToVO(user, t)) .collect(Collectors.toList()); } }这种方式的缺点是数据量大时内存压力大而且没法做分页。如果数据量确实很大可以考虑在 SQL Server 中建一个 MySQL 的链接服务器用OPENQUERY做跨库查询但配置起来比较麻烦而且性能不一定好。提示跨数据源查询尽量避免在循环中调用另一个数据源那样会产生 N1 问题。能批量查就批量查一次把需要的数据都取回来再在内存中处理。4.4 事务管理的实际使用单数据源事务很简单Transactional一加就行。双数据源下必须指定事务管理器Service public class OrderService { Autowired private OrderMapper orderMapper; Transactional(transactionManager mysqlTransactionManager, rollbackFor Exception.class) public void createOrder(Order order) { orderMapper.insert(order); // 其他 MySQL 操作 } Transactional(transactionManager sqlServerTransactionManager, rollbackFor Exception.class) public void syncReport(Report report) { reportMapper.insert(report); // 其他 SQL Server 操作 } }rollbackFor Exception.class是为了让受检异常也触发回滚。默认情况下Spring 只在运行时异常时回滚受检异常不回滚。这个坑我踩过当时一个 IO 异常导致数据写了一半没回滚排查了半天。5. 常见问题与排查技巧实录5.1 启动报错“多个 DataSource Bean”这是最常见的问题。原因是DataSourceAutoConfiguration自动装配了一个 DataSource你又手动声明了两个Spring 不知道该注入哪个。解决办法是在启动类上排除自动装配SpringBootApplication(exclude {DataSourceAutoConfiguration.class})或者用SpringBootApplication的exclude属性效果一样。5.2 Mapper 绑定到错误的数据源现象是明明在 MySQL 的 Mapper 里写了一条 SQL执行时却报“表不存在”因为这条 SQL 被发到了 SQL Server 上。根本原因是MapperScan的包路径写错了或者两个配置类的basePackages有重叠。检查方法是启动时看日志MyBatis 会打印每个 Mapper 绑定的 SqlSessionFactory。提示可以在配置类中加一行日志打印sqlSessionFactory.getConfiguration().getMappedStatements()确认每个 Mapper 都绑到了正确的数据源。5.3 SQL Server 驱动连接超时SQL Server 的默认连接超时是 30 秒但有时候网络波动会导致连接建立很慢。可以在 JDBC URL 中加loginTimeout10缩短超时时间避免请求堆积。另外SQL Server 的encrypt参数在开发环境可以设为false生产环境如果开了true但没配证书会报 SSL 错误。这时候要么配证书要么设trustServerCertificatetrue仅限内网可信环境。5.4 事务不生效的几种情况除了前面说的没指定事务管理器还有几种情况会导致事务不生效方法不是 public 的。Spring 的声明式事务基于代理私有方法、protected 方法上的Transactional不生效。同类内部调用。一个 Service 的方法 A 调用同类的方法 B方法 B 上的Transactional不生效因为调用没经过代理。异常被吞了。如果 catch 了异常没有重新抛出事务管理器感知不到异常不会回滚。5.5 常见问题速查表问题现象可能原因排查方向启动报多个 DataSource自动装配未排除检查启动类 excludeMapper 绑错数据源包路径重叠检查 MapperScan事务不回滚未指定事务管理器检查 TransactionalSQL Server 连接超时网络或驱动配置检查 JDBC URL 参数XML 映射找不到mapperLocations 未配检查资源路径中文乱码字符集未指定检查 URL 参数5.6 几个我踩过的坑第一个坑ConfigurationProperties不生效。一开始我把ConfigurationProperties加在DataSourceBuilder.create().build()返回的对象上结果配置没注入进去。后来发现必须加在Bean方法上而且方法返回类型要是DataSource不能是HikariDataSource。第二个坑SQL Server 的datetime类型映射。SQL Server 的datetime精度是 3.33msJava 的LocalDateTime精度是纳秒直接映射会有精度丢失。如果业务对时间精度敏感建议在 SQL Server 中用datetime2类型。第三个坑连接池泄漏。有一次压测发现连接数一直涨最后查出来是某个查询用了SqlSession没关闭。MyBatis-Spring 会自动管理 SqlSession 的生命周期但如果手动获取 SqlSession 就必须手动关闭。6. 进阶优化与扩展思路6.1 动态数据源切换如果两个数据源的切换逻辑比较复杂比如要根据用户身份动态选择可以考虑用AbstractRoutingDataSource实现动态数据源。核心思路是继承AbstractRoutingDataSource重写determineCurrentLookupKey()方法返回当前线程要用的数据源标识。public class DynamicDataSource extends AbstractRoutingDataSource { Override protected Object determineCurrentLookupKey() { return DataSourceContextHolder.getDataSource(); } }DataSourceContextHolder用ThreadLocal保存当前线程的数据源标识在 Service 方法执行前设置执行后清除。这种方式适合多租户场景但要注意事务和线程池的配合避免数据源标识串线程。6.2 读写分离的延伸双数据源的配置思路可以直接扩展到读写分离一个主库写多个从库读。配置方式和双数据源类似只是从库可能有多个需要用负载均衡策略选择。不过读写分离涉及主从同步延迟的问题如果业务对一致性要求高写完立刻读可能会读到旧数据。这种场景下要么强制走主库要么在应用层做延迟等待。6.3 监控与告警双数据源上线后监控要跟上。HikariCP 提供了HikariPoolMXBean可以获取活跃连接数、空闲连接数、等待线程数等指标。把这些指标接入 Prometheus 或你用的监控系统设置告警阈值。我一般会监控这几个指标活跃连接数持续超过最大连接数的 80%、等待获取连接的线程数大于 0、连接创建失败次数。这些指标异常往往意味着连接池配置不合理或者有连接泄漏。6.4 配置的版本管理双数据源的配置类代码量不小而且两个配置类结构高度相似。可以考虑抽一个抽象基类把公共逻辑抽出来子类只提供数据源名称和包路径。这样新增第三个数据源时只需要写一个很薄的子类。不过抽象也要适度。如果两个配置类的差异点太多强行抽象反而增加理解成本。我的经验是当数据源数量达到三个以上时再考虑抽象两个数据源直接复制粘贴更直观。提示配置类中的密码不要明文写在application.yml里。可以用环境变量或者配置中心至少也要用 Jasypt 之类的工具加密。我见过太多项目把生产密码提交到代码仓库了这是个很大的安全隐患。6.5 单元测试的写法双数据源的单元测试要注意测试类启动时会加载所有数据源配置如果某个数据库连不上整个测试类都会失败。解决办法是用MockBean把不需要测试的数据源 Mock 掉或者用 H2 内存数据库替代。如果确实需要连真实数据库做集成测试建议用 Testcontainers 启动临时的 MySQL 和 SQL Server 容器测试完自动销毁不依赖本地环境。SpringBootTest Testcontainers class DualDataSourceTest { Container static MySQLContainer? mysql new MySQLContainer(mysql:8.0); Container static MSSQLServerContainer? sqlServer new MSSQLServerContainer(mcr.microsoft.com/mssql/server:2019-latest); DynamicPropertySource static void configureProperties(DynamicPropertyRegistry registry) { registry.add(spring.datasource.mysql.jdbc-url, mysql::getJdbcUrl); registry.add(spring.datasource.sqlserver.jdbc-url, sqlServer::getJdbcUrl); } }这种写法虽然前期配置麻烦一点但测试的可靠性高不依赖开发人员的本地环境CI 上也能跑。6.6 关于性能的一点实测数据我在一台 4 核 8G 的测试机上做过简单压测单数据源MySQL的 QPS 大约是 3200加上 SQL Server 双数据源后MySQL 的 QPS 降到 2900 左右。下降的原因主要是连接池管理开销和配置类的初始化但 10% 左右的性能损耗在可接受范围内。如果对性能极其敏感可以考虑把两个数据源拆成两个独立的微服务各自用单数据源。但这样增加了服务间调用的网络开销和运维复杂度需要根据实际情况权衡。我个人在实际操作中的体会是双数据源配置本身不难难的是把包结构规划好、把事务边界理清楚。前期多花半小时设计包结构后期能省下大量排查问题的时间。另外配置类写完一定要写单元测试验证不要等到集成阶段才发现某个 Mapper 绑错了数据源。最后再分享一个小技巧在配置类中加一行启动日志打印每个数据源的实际连接 URL隐藏密码这样启动时一眼就能看出配置有没有生效。