项目跑得好好的,突然产品提了一个需求,说要把 PostgreSQL 里的业务数据和另一台 SQL Server 上的历史数据放到一起看。数据量不是特别大,但每次都开两个连接写代码又麻烦,于是“Spring Boot 多数据源”这几个字就被提上了日程。我正在做的一个数据交换服务就是这么干的,现在把连接 PostgreSQL 和 SQL Server 的实践过程完整整理出来,从依赖配置、动态切换、事务处理到排坑实录,一次讲透。
如果你最近也在搞 Spring Boot 多数据源,或者准备接 PostgreSQL 和 SQL Server 这两个库,这篇文章应该能帮你少走不少弯路。我会先用“为什么”的角度把方案选型讲清楚,再给出可以直接抄的代码和配置,最后把我在实操里踩过的坑一个个列出来。
1. 多数据源需求分析:不是所有项目都适合硬上
1.1 真实业务场景长什么样
多数据源并不是一个炫技的需求,而是现实逼迫的产物。最常见的情况有四种:
- 老系统用的是 SQL Server,新系统切换到了 PostgreSQL,在过渡阶段需要两边同时读。
- 业务库在 PostgreSQL,报表库在 SQL Server,服务层要同时查询两边然后聚合结果。
- 做数据清洗和迁移工具,比如定时从 SQL Server 拉取增量数据写入 PostgreSQL。
- 多个业务系统之间不开放数据库直连,只允许通过应用层做跨库“联表查询”。
这几种场景都有一个共同特点:你没办法在数据库层面直接做跨库 join,也没办法让 DBA 把数据同步好,最直接的做法就是在同一个 Spring Boot 进程里维护两个数据源。
有人会问,为什么不拆成两个独立服务?两个服务确实更干净,但如果只是为了在一个接口里返回聚合结果,拆服务会增加网络开销、部署成本和事务协调成本。在一段时间内,用一个应用同时连两个数据库,反而是比较划算的方案。
1.2 三种实现思路对比,别一上来就写代码
我在调研和实际落地过程中,见过下面这三种主流做法:
| 方案 | 实现思路 | 优点 | 缺点 |
|---|---|---|---|
| 手动获取连接 | 在代码里分别持有两个 DataSource,按业务硬编码调用 | 逻辑直白,无框架依赖 | 侵入性太强,每个方法都要写连接获取和异常处理 |
| 多套 SqlSessionFactory 分包扫描 | 按 mapper 包分开绑定不同数据源 | 数据源隔离彻底,事务边界清晰 | 配置量较大,同一个实体类要维护多套 Mapper |
| AbstractRoutingDataSource + AOP | 运行时通过路由键切换数据源 | 代码侵入小,原理清晰,主流方案 | 需要自己写上下文和切面,事务处理要小心 |
我的选择是第三种,也就是 Spring 官方提供的AbstractRoutingDataSource,结合自定义注解和 AOP 实现动态切换。这样做的核心原因有两个:第一,它不需要引入额外重量级框架,底层原理就是路由;第二,所有切换逻辑收敛到一个切面里,业务代码几乎无感,后期维护成本低。
2. 依赖和配置准备:版本兼容是第一道大坑
2.1 版本环境怎么选
先说环境。我当时用的是 Spring Boot 2.7.x + JDK 8,数据库这边 PostgreSQL 13,SQL Server 2019。这个组合比较稳,网上资料也最多。
如果你用 Spring Boot 3.x,那需要 JDK 17,并且 MyBatis-Plus 要升级到 3.5.3 以上才兼容jakarta命名空间。SQL Server 驱动方面,微软的mssql-jdbc从 9.x 开始只支持 Java 8+,到了 12.x 对老版本 SQL Server 的支持做了收窄,如果连接 SQL Server 2008 R2,建议用mssql-jdbc:7.4.0.jre8甚至更低版本。
PostgreSQL 驱动相对简单,直接使用org.postgresql:postgresql:42.6.0就行。它要求 PostgreSQL 8.2 以上,基本都能满足。
2.2 Maven 依赖引入
我使用的是 MyBatis-Plus,所以 pom 里引入了mybatis-plus-boot-starter。如果你不想用 MyBatis-Plus,换成mybatis-spring-boot-starter也可以,核心的数据源逻辑完全一样。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>42.6.0</version> </dependency> <dependency> <groupId>com.microsoft.sqlserver</groupId> <artifactId>mssql-jdbc</artifactId> <version>12.2.0.jre8</version> </dependency>这里有个容易踩的坑:如果连接老版本 SQL Server 2008 R2,mssql-jdbc12.x 很可能尝试连接时直接报“驱动程序不支持此 SQL Server 版本”。所以本地开发要先确认目标库版本,再决定驱动版本。不要盲目用最新的驱动。
2.3 application.yml 双数据源配置
Spring Boot 常规配置只认spring.datasource.url,但多数据源时我们会用自定义前缀spring.datasource.primary和spring.datasource.secondary,配合@ConfigurationProperties绑定。
spring: datasource: primary: jdbc-url: jdbc:postgresql://localhost:5432/business_db username: postgres password: postgres driver-class-name: org.postgresql.Driver hikari: pool-name: HikariPool-PG maximum-pool-size: 10 minimum-idle: 2 secondary: jdbc-url: jdbc:sqlserver://localhost:1433;DatabaseName=history_db;encrypt=true;trustServerCertificate=true username: sa password: YourStrongPass driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver hikari: pool-name: HikariPool-MSSQL maximum-pool-size: 5 minimum-idle: 1注意两个地方:
第一,自定义前缀的 HikariCP 配置,字段必须用jdbc-url,不能用url。因为 Spring Boot 的DataSourceBuilder在绑定HikariDataSource时,只认jdbc-url,写成url会导致连接池启动时报错或者根本读不到连接地址。这个细节坑了好几个人。
第二,SQL Server 连接串里面的参数用分号分隔,而不是?接 query string。DatabaseName=history_db指定库名;encrypt=true;trustServerCertificate=true是微软驱动对 SSL 加密的应对方案,开发环境先这样放开,生产环境请使用真实证书。
2.4 Spring Boot 高版本注意事项
现在不少人直接上 Spring Boot 3.x,结果发现老一套代码跑不起来。这里单独说几句。
Spring Boot 3 从javax迁移到了jakarta,如果你用 MyBatis-Plus 3.4.x,会直接抛 ClassNotFound。所以要么老老实实待在 Boot 2.7.x + JDK 8,要么把 MyBatis-Plus、SQL Server 驱动、PostgreSQL 驱动全部升级到对应的高版本。
另外,Spring Boot 3.x 里配置 SQL Server 连接串时,同样要注意新版驱动默认要求 TLS 加密。很多人在本地连 SQL Server 报“The driver could not establish a secure connection to SQL Server by using Secure Sockets Layer (SSL) encryption”,就是因为没有加trustServerCertificate=true。
3. 核心实现:用 AbstractRoutingDataSource 做动态数据源
3.1 路由原理:其实就是一张 Map 加一个线程变量
AbstractRoutingDataSource是 Spring 提供的一个抽象类,内部维护了两个关键字段:targetDataSources和defaultTargetDataSource。targetDataSources是一个 Map,key 是路由键,value 是真实的数据源对象。
当调用getConnection()时,Spring 会先调用determineCurrentLookupKey()拿当前路由键,然后用这个键去 Map 里找真正要用的数据源。我们要做的事情很清晰:
- 定义两个路由键,分别对应 PostgreSQL 和 SQL Server。
- 用一个 ThreadLocal 保存当前线程的路由键。
- 在业务方法执行前设置路由键,执行后清理。
- 用 AOP 统一处理“设置”和“清理”两个动作。
3.2 先定义枚举和线程上下文
public enum DataSourceType { PRIMARY, SECONDARY }public class DataSourceContextHolder { private static final ThreadLocal<DataSourceType> CONTEXT = new ThreadLocal<>(); public static void set(DataSourceType dataSourceType) { CONTEXT.set(dataSourceType); } public static DataSourceType get() { return CONTEXT.get() == null ? DataSourceType.PRIMARY : CONTEXT.get(); } public static void clear() { CONTEXT.remove(); } }ThreadLocal是这里的关键。因为 Web 请求的每个线程都独立保存上下文,不会互相污染;同时 Spring Boot 的线程池会复用线程,所以一定要在 finally 里clear(),否则下一次复用线程时会串库。
3.3 编写动态数据源类
public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DataSourceContextHolder.get(); } }这段代码非常短,但所有“选择哪个库”的逻辑都在这里。返回DataSourceType.PRIMARY就会用 PostgreSQL,返回DataSourceType.SECONDARY就会用 SQL Server。
3.4 组装 DataSource Bean
接下来把两个真实数据源和动态数据源注册到 Spring 容器中。
@Configuration public class DataSourceConfig { @Bean @ConfigurationProperties("spring.datasource.primary") public DataSource primaryDataSource() { return DataSourceBuilder.create().build(); } @Bean @ConfigurationProperties("spring.datasource.secondary") public DataSource secondaryDataSource() { return DataSourceBuilder.create().build(); } @Bean @Primary public DynamicDataSource dynamicDataSource() { DynamicDataSource dynamicDataSource = new DynamicDataSource(); Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put(DataSourceType.PRIMARY, primaryDataSource()); targetDataSources.put(DataSourceType.SECONDARY, secondaryDataSource()); dynamicDataSource.setTargetDataSources(targetDataSources); dynamicDataSource.setDefaultTargetDataSource(primaryDataSource()); return dynamicDataSource; } }这里有两个关键点:
dynamicDataSource必须标注@Primary,否则 Spring 容器里有多个DataSourceBean,MyBatis 自动配置和事务管理器都不知道该用哪一个。targetDataSources的 key 用枚举对象本身即可,因为determineCurrentLookupKey()返回的也是同一个枚举对象。
3.5 自定义注解和 AOP 切面
为了让业务代码无感,我会定义@DataSource注解,然后用 AOP 在方法调用前后自动切换数据源。
@Documented @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface DataSource { DataSourceType value() default DataSourceType.PRIMARY; }切面这里我推荐用@Around,比@Before+@After更安全,因为它可以在 finally 里保证清理上下文,即使方法抛异常也不会漏。
@Aspect @Component @Order(-1) public class DataSourceAspect { @Around("@annotation(dataSource)") public Object around(ProceedingJoinPoint joinPoint, DataSource dataSource) throws Throwable { DataSourceContextHolder.set(dataSource.value()); try { return joinPoint.proceed(); } finally { DataSourceContextHolder.clear(); } } }@Order(-1)的意思是这个切面的优先级要比事务切面更高。这样在事务管理器开始事务之前,路由键就已经设置好了。
3.6 在 Service 层使用
@Service public class OrderQueryService { @Autowired private OrderMapper orderMapper; @Autowired private OrderArchiveMapper orderArchiveMapper; @DataSource(DataSourceType.PRIMARY) public Order getOrderFromPg(Long orderId) { return orderMapper.selectById(orderId); } @DataSource(DataSourceType.SECONDARY) public List<OrderArchive> getArchivesFromSqlServer(Long userId) { return orderArchiveMapper.selectList( new LambdaQueryWrapper<OrderArchive>() .eq(OrderArchive::getUserId, userId)); } }使用的时候,只需要在方法上加注解,方法内部不用写任何数据源切换代码。PostgreSQL 的操作走PRIMARY,SQL Server 的操作走SECONDARY,互不干扰。
这里要提醒一个 AOP 经典问题:同一个类内部调用带@DataSource的方法,注解不会生效。比如this.getOrderFromPg(orderId),因为 Spring AOP 是基于代理的,内部调用绕过了代理对象。解决办法是把不同数据源的方法拆到不同的 Bean 里,或者用AopContext.currentProxy()。
4. 参数调整和事务问题:不能只配置完就完事
4.1 连接池参数怎么给两个库分配
多数据源环境下,两个连接池是互相独立的,所以参数也要分开设计。不要图省事给两个库配一样的连接数。
PostgreSQL 作为主业务库,并发通常较高,我一般会设置maximum-pool-size: 10,minimum-idle: 2,connection-timeout: 30000。SQL Server 作为历史库或报表库,查询频率低,连接数给5就够。连接池大小不是越多越好,每一条连接都占着内存和数据库资源,配合实际 QPS 估算才是正路。
HikariCP 还有一个常用参数pool-name,建议给每个连接池取不同的名字,这样在日志和监控里能一眼看出当前请求用的是哪个库的连接。
4.2 MyBatis 如何自动使用动态数据源
只要我们在DataSourceConfig里定义了DynamicDataSource并标注了@Primary,MyBatis-Plus 的自动配置就会把DynamicDataSource作为唯一的DataSource注入到SqlSessionFactory里。
这就意味着我们不需要为 PostgreSQL 和 SQL Server 分别创建SqlSessionFactory。MyBatis 持有一个动态数据源引用,每次执行 SQL 的时候根据路由键选择真实连接。
如果你用的不是 MyBatis-Plus,而是原生 MyBatis,也是一样的原理。只要DataSourceBean 唯一,MyBatis 自动配置就会生效。
如果项目比较复杂,比如 PostgreSQL 和 SQL Server 两边的表结构差异很大,并且你想让 Mapper 接口按包分开扫描,那才需要配置多个SqlSessionFactory。这种情况一般配两个DataSource,而不是一个动态数据源。两种姿势没有绝对好坏,看团队熟悉程度和维护成本。
4.3 事务管理器与跨数据源事务
多数据源最需要注意的是事务,这不是开个玩笑,我见过不少人在这一步翻车。
默认情况下,Spring Boot 会创建一个绑定到唯一DataSource的DataSourceTransactionManager。由于我们只有一个DynamicDataSource,所以事务管理器绑定到动态数据源上,本身没问题。但你要理解它背后发生了什么:
- 事务开始时,事务管理器从
DynamicDataSource获取连接。 DynamicDataSource.getConnection()会调用determineCurrentLookupKey()。- 如果当前线程已经被
@DataSource切面设置了SECONDARY,那么事务连接来自 SQL Server。 - 这个连接会被绑定到当前线程,直到事务提交或回滚。
所以这里有一个很容易被忽略的限制:同一个事务里的连接一旦确定,就不会因为你中途切换数据源而切换。如果业务方法先查了 PostgreSQL,又在同一个事务里(通过内部方法切换)去查 SQL Server,后半段查询拿到的仍然是 PostgreSQL 的连接,整条链路会乱掉。
解决办法有三种:
- 把跨库操作拆成两个独立 Service 方法,分别管理事务。
- 使用
@Transactional时,保证一个事务内只操作一个数据源。 - 如果业务上确实需要强一致性跨库事务,需要引入 JTA 或 Seata,用本地应用多数据源方案是无法保证的。
如果你需要配置事务管理器,可以显式声明:
@Configuration public class TransactionConfig { @Bean @Primary public PlatformTransactionManager transactionManager(DynamicDataSource dynamicDataSource) { return new DataSourceTransactionManager(dynamicDataSource); } }5. 实操中常见的坑与排查实录
5.1 PostgreSQL 启动时报“无法创建锁文件 /var/run/postgresql/.s.pgsql.5432.lock: 权限不够”
这个问题热词里出现了很多次,本质上不是 Spring Boot 的问题,而是 PostgreSQL 进程没有权限在/var/run/postgresql目录下创建 Unix Socket 文件。
我当时的处理方式分两步:
如果服务器上已经有
postgres系统用户,用这个用户启动 PostgreSQL,而不要用 root 或当前普通用户。如果目录权限不对,执行:
sudo mkdir -p /var/run/postgresql sudo chown postgres:postgres /var/run/postgresql sudo chmod 1777 /var/run/postgresql如果实在不想碰系统目录,还可以在
postgresql.conf里修改:unix_socket_directories = '/tmp'
然后重启数据库。Spring Boot 连接的是 TCP 端口,锁文件只影响 Unix Socket 连接,但很多本地测试工具会走 Socket,所以这个问题必须处理掉。
5.2 SQL Server 用户 'sa' 登录失败
刚开始配置 SQL Server 数据源时,最常见的是sa登录失败。Spring Boot 里报错通常是:
Login failed for user 'sa'. ClientConnectionId:xxxx这基本都是在 SQL Server 侧没开混合认证模式,或者sa账号本身是禁用状态。排查步骤:
- 打开 SQL Server Management Studio,右键实例进入“属性”。
- 在“安全性”页签,把服务器身份验证改为“SQL Server 和 Windows 身份验证模式”。
- 展开“安全性 -> 登录名 -> sa”,右键进入属性。
- 在“状态”页签启用登录,并在“常规”页签重置密码。
- 最后重启 SQL Server 服务,混合认证才会生效。
另外,SQL Server 默认可能只监听 1433 端口,如果你连接的是命名实例,需要先启动 SQL Server Browser 服务。端口不对时,错误信息里会带上Named Pipes Provider: Could not open a connection to SQL Server,和热词里的[08001]很像。
5.3 SQL Server ODBC 驱动报 08001,Named Pipes Provider 连不上
虽然 Spring Boot 用的是 JDBC,不是 ODBC,但很多人在用 DBeaver、DataGrip 或 Windows 工具时遇到过这个[08001] [microsoft][odbc driver 17 for sql server]named pipes provider错误。
出现这个错,说明客户端尝试用 Named Pipes 协议连接,但服务器端没开对应协议,或者主机名/实例名解析有问题。解决办法是把连接串改成直连端口的形式,避免依赖 SQL Browser:
jdbc:sqlserver://192.168.1.100:1433;DatabaseName=history_db;encrypt=true;trustServerCertificate=true如果数据库实例不是默认实例,比如192.168.1.100\SQLEXPRESS,且无法直连端口,则要确认 SQL Server Browser 服务是启动状态,并且在防火墙里放行 UDP 1434。
5.4 数据源切换不生效怎么办
动态数据源看起来代码不多,但切换不生效的问题非常典型。
我会从这几个方向排查:
- 确认
DataSourceAspect被 Spring 管理,也就是类上有没有@Component。 - 确认业务方法是通过代理对象调用的,而不是同类内部
this.xxx()调用的。 - 确认
ThreadLocal有没有在方法结束后被清理。可以用@Around+finally,这样最稳。 - 如果同一个请求里需要多个数据源切换,可以考虑在 Service 方法内部通过
DataSourceContextHolder.set()手动切换,并在 finally 里清理。 - 打开日志,在
DataSourceContextHolder.set()和clear()里打印当前线程名称和数据源类型,排查串库。
我一般会加一行日志:
logger.debug("switch datasource thread={}, type={}", Thread.currentThread().getName(), dataSourceType);这样线上问题定位很快。
5.5 Spring Boot 版本太高与 MyBatis-Plus 多数据源插件
如果你不想自己写 AOP,MyBatis-Plus 官方提供了dynamic-datasource-spring-boot-starter,使用起来更省事:
<dependency> <groupId>com.baomidou</groupId> <artifactId>dynamic-datasource-spring-boot-starter</artifactId> <version>3.5.2</version> </dependency>使用这个插件后,可以直接在 Service 方法上写@DS("primary")或@DS("secondary"),配置类也简化不少。但我个人建议先理解AbstractRoutingDataSource的原理,再决定是否用插件。因为插件的底层核心仍然是路由,跟本文的思路一样,只是把上下文管理和切面包得好了一点。遇到问题时不理解原理,很容易被“灵异现象”卡住。
另外,Spring Boot 3.x 用户注意插件的版本兼容性。dynamic-datasource-spring-boot-starter3.5.x 对 Spring Boot 3 的支持已经比较成熟,但 3.4.x 及以下版本可能报错。升级前先看官方 Release 说明。
6. 最后的实操建议
多数据源连接 PostgreSQL 和 SQL Server 这条路,我走过几次之后,总结出几个小习惯:
先拿 DBeaver 或 DataGrip 把两个库的连接串、账号密码全部验证一遍,再写 Spring Boot 代码。很多时候连接不上不是代码问题,而是数据库端口、账号权限、SSL 设置的问题。连接串能通过工具连接,基本能省掉 80% 的排查时间。
连接池参数一定要分开配,宁可先往小了配,也不要无脑给 50。尤其是 SQL Server 历史库,并发不高时连接数给 5 就够了。连接数太大反而容易把数据库连接吃满,影响其他业务。
如果是 Spring Boot 3.x 用户,建议在 pom 里锁定驱动版本,不要用依赖传递隐式带的版本。SQL Server 驱动新旧版本差异很大,PostgreSQL 驱动相对稳,但也不能完全不看。
多数据源的事务尽量保持简单。一个事务里只碰一个库,跨库操作拆开做最终一致,是当前业务系统里比较稳妥的做法。不要指望在一个事务里先更新 PostgreSQL 再更新 SQL Server,然后统一回滚,那是分布式事务的领域,不是普通动态数据源能解决的。
最后再分享一个小技巧:在动态数据源类里临时加一行打印,把determineCurrentLookupKey()的返回值打出来,再去看数据库监控里的连接来源,排障效率会高很多。等系统稳定了再把这行日志调成 trace 级别,随时可以打开。