☰
Spring Boot双数据源:SQL Server与PostgreSQL实战
2026/10/23 3:56:10 网站建设 项目流程

1. 先把"两个库"这件事想清楚,再动手改配置

Spring Boot 多数据源配置这件事,我前后做过四五次,每次的翻车点都不一样。最近一次是接手一个已经跑了六年的业务系统:主库在 SQL Server 上,历史数据和报表逻辑全在里面;新上的数据采集模块要求用 PostgreSQL,因为要存 JSONB 和做复杂聚合。两边都得连,还得在同一个 Spring Boot 服务里共存,业务代码不能写成两套。这篇文章就把 Spring Boot 集成 SQL Server 与 PostgreSQL 双数据源的完整过程拆开讲——从为什么这么设计、连接参数怎么填、动态路由怎么实现、事务边界怎么划,到我自己踩过的那些坑。

如果你正在面对"老库不能动、新库必须上"的局面,或者单纯想搞清楚AbstractRoutingDataSource到底该怎么用、@Transactional为什么在多数据源下会失效,这篇内容应该能省你不少时间。需要说明的是,文中涉及的表名、库名、IP 都是占位示例,参数值请按你自己的环境替换。

1.1 什么情况下多数据源是真需求,什么情况下是自找麻烦

先说判断标准。多数据源不是"看起来高级"就上,它带来的复杂度是实打实的:连接池要维护两套、事务管理器要配两个、ORM 框架的会话工厂要建两个、监控指标要分两路看。所以我一般用三个问题来筛:

第一个问题,数据能不能迁。如果两个库的数据量都不大、业务耦合不深,老老实实做一次数据迁移,统一到一个库上,长期维护成本会低一个数量级。我见过太多团队为了"省迁移的两周"结果在多数据源上耗了半年。

第二个问题,写入是否交叉。如果只是"读老库、写新库",那本质上是一个只读数据源加一个读写数据源,复杂度和真正的双写完全不是一个档次。反过来,如果一个业务操作要同时写两张表且分属两个库,那就要认真考虑分布式事务或者补偿机制了。

第三个问题,团队能不能接受"配置地狱"。多数据源意味着启动类上要排除自动配置、@Primary要精确标注、@Transactional要指定管理器名,任何一个新人漏写一处,表现出来的可能就是"数据莫名写到了另一个库"。这种问题排查起来极其痛苦。

在我这个项目里,三个问题的答案分别是:不能迁(历史数据 800G,业务方不接受停机窗口)、有交叉(采集任务要把结果回写主库的统计表)、能接受(团队就四个人,约定写清楚就行)。所以我选择了多数据源,但不是全功能的多数据源,而是主库走默认路径、新库走注解显式切换的收敛方案。

1.2 三种实现路线的取舍:路由式、双套 ORM、还是拆服务

确定要做之后,落地路线有三条,我在选型阶段把每条都摸了一遍。

路线一:动态数据源路由。核心是继承AbstractRoutingDataSource,用一个ThreadLocal存当前数据源标识,每次取连接时根据标识从 Map 里挑一个真实数据源返回。优点是业务代码几乎无侵入,一个注解就能切库;缺点是对事务不友好,事务开启后再切换是无效的,必须保证切换发生在事务之前。

路线二:双套 ORM 完全隔离。每个数据源配一套DataSource+SqlSessionFactory+MapperScan+TransactionManager,Mapper 分包存放,互相不干扰。优点是边界极其清晰,永远不会串库;缺点是代码重复度高,两套 Mapper 里如果有相似逻辑,维护起来很烦。

路线三:拆成两个服务。从架构上最干净,但需要引入服务间调用、网络延迟、数据一致性处理,对一个只有四个人的团队来说属于过度设计。

我最终选的是路线一为主、路线二兜底的混合方案:常规业务查询用注解路由,跨库批量写入这类高风险操作单独配一套独立的SqlSessionFactory,走专用通道。这样既保留了开发效率,又给关键链路上了保险。这个决定后面救了我一次——批量回写主库的那段代码,如果走路由方案,在异步线程里切库会丢上下文,独立通道就没这个隐患。

2. 依赖、连接串与配置组织:魔鬼全在细节里

动手之前,先把依赖和连接参数搞对。这两块看着简单,实际上 SQL Server 和 PostgreSQL 的驱动在默认行为上差异很大,很多"连不上""乱码""超时"的问题根子都在这里。

2.1 驱动版本与连接池的版本对齐

依赖上没什么玄学,但版本要卡准。SQL Server 的 JDBC 驱动从 9.x 开始分成 jre8 和 jre11 两个分支,选错会在启动时报UnsupportedClassVersionError。PostgreSQL 驱动相对省心,但要注意 42.x 系列在 SSL 默认行为上有过变化。

<dependency> <groupId>com.microsoft.sqlserver</groupId> <artifactId>mssql-jdbc</artifactId> <version>12.6.1.jre11</version> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>42.7.3</version> </dependency> <dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> </dependency>

连接池我统一用 HikariCP,理由很实际:Spring Boot 默认就是它,两套数据源共用一种池实现,参数含义一致,排查问题时不用在两种池的术语之间来回翻译。Druid 的监控面板确实好看,但多数据源场景下要配两个StatViewServlet,还得处理监控本身的线程安全,收益不大。

提示:如果项目里用了spring-boot-starter-data-jpa,它会自带一个 HikariCP 版本,显式声明依赖时不要覆盖成差异过大的版本,否则可能出现NoSuchMethodError。

2.2 两个库的连接串,坑位完全不一样

SQL Server 的连接串是最容易出问题的地方。从 10.x 驱动开始,encrypt默认为true,也就是说即使你连的是内网一台没配证书的实例,驱动也会尝试加密握手,然后抛The driver could not establish a secure connection to SQL Server by using Secure Sockets Layer (SSL) encryption。解决办法有两个:生产环境老老实实配证书;测试和内网环境显式加trustServerCertificate=true。

jdbc:sqlserver://10.0.0.11:1433;databaseName=erp_main;encrypt=true;trustServerCertificate=true;loginTimeout=5;applicationName=erp-service

PostgreSQL 那边的问题主要在 schema 和时区。默认 schema 是public,如果业务表建在别的 schema 下,不显式指定就会报"表不存在",而且报错信息往往只告诉你表名,不告诉你去哪找。

jdbc:postgresql://10.0.0.12:5432/collect_db?currentSchema=collect&reWriteBatchedInserts=true&ApplicationName=erp-service

reWriteBatchedInserts=true这个参数值得单独说。PostgreSQL 驱动在开启后会把多条INSERT重写成一条多值插入,批量写入性能提升非常明显,我实测 5000 条一批从 3.2 秒降到 0.6 秒左右。SQL Server 那边对应的是useBulkCopyForBatchInsert=true,原理是走 TDS 协议的 Bulk Copy,效果同样显著,但它对字段类型有要求,遇到datetimeoffset之类的类型可能回退到普通模式。

2.3 配置文件的组织方式,别用默认前缀

这里有个非常经典的坑:自定义前缀的数据源不能用url,必须用jdbc-url。

原因在于spring.datasource.url是DataSourceProperties的属性,Spring Boot 会自动把它转换成 Hikari 需要的jdbcUrl。但当你用@ConfigurationProperties("spring.datasource.primary")直接绑定到HikariDataSource时,绑定的是 Hikari 自己的 setter,而 Hikari 的字段名是jdbcUrl,写url会绑定失败且不报错,最后表现是"连接串为 null"。

spring: datasource: primary: jdbc-url: jdbc:sqlserver://10.0.0.11:1433;databaseName=erp_main;encrypt=true;trustServerCertificate=true username: app_user password: ${DB_PWD} driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 3000 idle-timeout: 600000 max-lifetime: 1700000 connection-test-query: SELECT 1 collect: jdbc-url: jdbc:postgresql://10.0.0.12:5432/collect_db?currentSchema=collect&reWriteBatchedInserts=true username: collect_user password: ${DB_PWD_COLLECT} driver-class-name: org.postgresql.Driver hikari: maximum-pool-size: 12 minimum-idle: 3 connection-timeout: 3000 max-lifetime: 1500000 connection-test-query: SELECT 1

max-lifetime要设得比数据库或中间层的空闲断连时间短一截。SQL Server 侧我按 1700 秒设(约 28 分钟),PostgreSQL 侧 1500 秒,都是留了余量之后的保守值。这个参数不设的后果是:连接被对端悄悄掐掉,池子还以为它活着,取出来一用就报Connection reset,而且往往是低峰期偶发,最难查。

连接池大小我做了一次粗算。主库的核心接口 QPS 峰值约 180,单次事务平均持有连接 25 毫秒,按利特尔法则并发占用约为180 × 0.025 ≈ 4.5,考虑到慢查询和批量的长事务,取 20 是比较稳的。新库那边 QPS 只有 30 左右,但单次任务耗时长(批量插入 2 到 5 秒),并发连接需求反而更高,所以给了 12。这两个数字加起来要小于数据库实例的max_connections减去留给运维和其他应用的余量,这一点在给 DBA 报备时一定要算清楚。

3. 动态数据源的完整实现与事务顺序

这一节是整篇文章的核心。多数据源能不能用得稳,八成取决于路由和事务的配合是否正确。

3.1 数据源 Bean 的构建与主数据源的确定

用DataSourceBuilder创建两个HikariDataSource,其中一个标@Primary。这个注解不能省,也不能标错——Spring 在注入DataSource类型时如果发现多个候选又没有主候选,会直接启动失败。

@Configuration public class DataSourceConfig { @Bean("primaryDataSource") @Primary @ConfigurationProperties(prefix = "spring.datasource.primary") public DataSource primaryDataSource() { return DataSourceBuilder.create().type(HikariDataSource.class).build(); } @Bean("collectDataSource") @ConfigurationProperties(prefix = "spring.datasource.collect") public DataSource collectDataSource() { return DataSourceBuilder.create().type(HikariDataSource.class).build(); } }

如果项目里有DataSourceAutoConfiguration在跑,它会尝试按spring.datasource.*去创建一个默认数据源,跟我们定义的重名或冲突。稳妥的做法是在启动类上排除掉:

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})

排除之后,MyBatis 或 JPA 的自动配置也会跟着失效,需要手动配置SqlSessionFactory。这部分在第四节展开。

3.2 用 AbstractRoutingDataSource 实现运行时切换

AbstractRoutingDataSource的思路很朴素:它本身是个DataSource,内部维护一个Map<Object, DataSource>和一个默认数据源,每次getConnection()时调用determineCurrentLookupKey()拿到 key,从 Map 里取出真实数据源再委托过去。

public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DataSourceContextHolder.get(); } } public final class DataSourceContextHolder { private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>(); public static void set(String key) { CONTEXT.set(key); } public static String get() { return CONTEXT.get(); } public static void clear() { CONTEXT.remove(); } }

装配的时候把两个真实数据源塞进 Map,默认指向主库:

@Bean("dynamicDataSource") @Primary public DataSource dynamicDataSource( @Qualifier("primaryDataSource") DataSource primary, @Qualifier("collectDataSource") DataSource collect) { Map<Object, Object> targets = new HashMap<>(4); targets.put("primary", primary); targets.put("collect", collect); DynamicDataSource ds = new DynamicDataSource(); ds.setTargetDataSources(targets); ds.setDefaultTargetDataSource(primary); return ds; }

有个细节容易被忽略:AbstractRoutingDataSource在afterPropertiesSet()里才把targetDataSources解析成resolvedDataSources,如果你在 Bean 初始化之后再往 Map 里塞东西是不生效的。另外它的getConnection()是在每次调用时才做路由判断,这一点对理解后面的时序问题很关键。

注意:DataSourceContextHolder用完必须clear()。我见过因为线程池复用导致的"幽灵切库"——某个请求切到新库,处理完没清上下文,线程被还回池子,下一个请求拿到同一个线程,莫名其妙去查了新库,报"表不存在"。这种问题在压测时几乎复现不出来,只有线上偶发。

3.3 注解加切面,把切库变成一行代码

裸用DataSourceContextHolder.set()到处写太丑,封装一个注解:

@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface DS { String value() default "primary"; }

切面里最关键的是执行顺序。Spring 的@Transactional默认切面顺序是Ordered.LOWEST_PRECEDENCE,如果我们的切面顺序排在它后面,就会出现"事务已经开启并绑定了连接,然后才切库"的情况,切换完全无效。

@Aspect @Component @Order(Ordered.HIGHEST_PRECEDENCE) public class DataSourceAspect { @Around("@annotation(ds)") public Object around(ProceedingJoinPoint pjp, DS ds) throws Throwable { String origin = DataSourceContextHolder.get(); try { DataSourceContextHolder.set(ds.value()); return pjp.proceed(); } finally { if (origin == null) { DataSourceContextHolder.clear(); } else { DataSourceContextHolder.set(origin); } } } }

这里的finally里做的是"恢复现场"而不是简单清理,为了支持嵌套调用——A 方法切到新库,内部调用 B 方法又切回主库,B 执行完应该回到新库而不是主库。这个细节很多示例代码都没写,一旦业务里出现多层调用就会出问题。

3.4 让切换在事务内也生效:LazyConnectionDataSourceProxy

上面那套方案有个硬限制:只能在事务外切库。因为@Transactional一开,Spring 就从数据源取一个连接绑到当前线程,后面的切库只是改了 ThreadLocal,连接不会换。

绕过的办法是LazyConnectionDataSourceProxy。它包装动态数据源,事务开始时并不真正取连接,只返回一个代理,等到第一次真正执行 SQL 时才去getConnection()。这样即使事务先开、切库后发生,最终拿到的仍然是正确的连接。

@Bean("lazyDataSource") @Primary public DataSource lazyDataSource(@Qualifier("dynamicDataSource") DataSource dynamic) { return new LazyConnectionDataSourceProxy(dynamic); }

代价是事务的隔离级别和只读属性在第一次取连接时才能生效,某些依赖元数据提前判断的场景(比如 Hibernate 的方言探测)可能受影响。我的做法是:主链路不加这层,保持简单;只有少数确实需要在事务内跨库读取的场景才走一个单独包装的 Bean。

4. ORM 层的适配:方言、分页与主键策略

数据源通了不代表能跑,SQL Server 和 PostgreSQL 在 SQL 方言上的差异足够让一套 Mapper 报错半天。

4.1 MyBatis 双会话工厂的正确拆法

路由式数据源只需要一个SqlSessionFactory,因为它面向的是那个动态数据源。但前面提到我额外留了独立通道,所以要多配一套。

@Bean("collectSqlSessionFactory") public SqlSessionFactory collectSqlSessionFactory( @Qualifier("collectDataSource") DataSource ds) throws Exception { MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean(); bean.setDataSource(ds); bean.setMapperLocations( new PathMatchingResourcePatternResolver() .getResources("classpath*:mapper/collect/**/*.xml")); // 关键:给新库指定方言,否则分页插件按主库语法生成 SQL MybatisConfiguration cfg = new MybatisConfiguration(); cfg.setDatabaseId("postgresql"); bean.setConfiguration(cfg); return bean.getObject(); }

MapperScan要把两个包分开,用basePackages和sqlSessionFactoryRef分别指定。如果两个 Mapper 包有交叉,就会踩到"Mapper 被重复注册"的坑,报Mapped Statements collection already contains value。

4.2 两个库的语法差异对照表

下面这张表是我在实际迁移中逐条验证过的,建议动手前先过一遍:

差异点SQL ServerPostgreSQL处理建议
分页OFFSET n ROWS FETCH NEXT m ROWS ONLY,必须带ORDER BYLIMIT m OFFSET n,无排序要求用 MyBatis-Plus 或 PageHelper 按方言自动生成,别手写
自增主键IDENTITY(1,1),插入后SELECT SCOPE_IDENTITY()GENERATED BY DEFAULT AS IDENTITY或SERIAL,RETURNING id用useGeneratedKeys="true"+keyProperty,两边都能兼容
布尔类型BIT,值为 0/1BOOLEAN,值为 true/false实体字段统一用Boolean,不要用int
时间类型DATETIME2精确到 100ns,无时区TIMESTAMPTZ带时区,TIMESTAMP不带统一用带时区的类型存绝对时间,业务层做展示转换
标识符大小写不区分,保留原样未加引号时全部折叠为小写表名和字段名统一小写下划线,别用驼峰
字符串拼接+或CONCAT`
空值排序NULL排在最前默认NULL排最后,需NULLS LAST需要固定顺序时显式写
批量插入useBulkCopyForBatchInsert=truereWriteBatchedInserts=true两个参数都要开,实测差 3 到 5 倍
字符串比较默认不区分大小写(取决于排序规则)默认区分大小写需要模糊匹配时用LOWER()包一层

表里最容易翻车的是"标识符大小写"这一条。PostgreSQL 里写SELECT userId FROM User会被解析成select userid from user,直接报错。我当时的做法是把所有建表语句和 Mapper 里的字段名统一改成小写蛇形,虽然改了两天,但一次性解决了后续所有隐患。

4.3 主键与批量插入的性能实测

自增主键这块,MyBatis 的useGeneratedKeys在两个库上都能用,但 PostgreSQL 走的是RETURNING,批量插入时能一次拿回所有主键;SQL Server 的SCOPE_IDENTITY()只返回最后一条,批量场景下要么逐条插,要么用OUTPUT INSERTED.id。这个差异导致批量写入的 Mapper 不能共用,我在新库那边单独写了一个insertBatch返回List<Long>。

实测数据(10000 条,单条 8 个字段,本机万兆内网):

方式SQL ServerPostgreSQL
逐条插入41.2 秒38.6 秒
批量(每批 1000,未开优化参数)6.8 秒7.1 秒
批量(每批 1000,开优化参数)1.9 秒1.4 秒

这个差距足以说明为什么那两个参数必须开。需要注意的是,批量参数开启后如果单批过大,可能触发数据库端的日志膨胀,PostgreSQL 尤其明显。我一般把批次控制在 500 到 1000 之间,超过 2000 反而会变慢。

5. 事务边界的划分与现实取舍

多数据源下的事务是最容易想当然的地方。很多人第一反应是"上分布式事务",实际落地时会发现成本远超收益。

5.1 单库事务:每个数据源一个管理器

不要试图用一个DataSourceTransactionManager管两个库,它做不到。正确做法是每个数据源配一个,@Primary标在主库那个上,其他显式指定名字。

@Bean("primaryTxManager") @Primary public PlatformTransactionManager primaryTxManager( @Qualifier("primaryDataSource") DataSource ds) { return new DataSourceTransactionManager(ds); } @Bean("collectTxManager") public PlatformTransactionManager collectTxManager( @Qualifier("collectDataSource") DataSource ds) { return new DataSourceTransactionManager(ds); }

用的时候必须写全:@Transactional(transactionManager = "collectTxManager", rollbackFor = Exception.class)。漏写transactionManager的后果是默认用主库管理器,然后你的新库操作压根不在事务里,出异常不回滚,而且没有任何报错提示。这个坑我踩过一次,损失是两万多条脏数据。

还有一点,rollbackFor要显式写Exception.class。Spring 默认只对RuntimeException和Error回滚,受检异常不回滚,这个默认行为在多数据源场景下更容易被忽略。

5.2 跨库写入:放弃强一致,改用补偿

如果业务必须同时写两个库,我不建议上 XA。原因很实在:SQL Server 和 PostgreSQL 的 XA 实现细节差异较大,Spring 的JtaTransactionManager需要额外的 JTA 实现,配置复杂度和运维成本都不低,而且两阶段提交的性能损耗在批量场景下很难接受。

我在项目里用的是"本地事务 + 事务性消息 + 幂等重试"的组合。具体做法是:主库操作和消息落表在同一个本地事务里,提交后由后台任务扫描消息表投递到新库,新库侧保证幂等(用业务唯一键加唯一索引)。失败就重试,超过阈值进死信表人工介入。

这套方案的缺点是存在短暂不一致窗口,一般是秒级到分钟级。评估时要问清楚:业务能不能接受这个窗口?我这个场景是报表统计,能接受。如果是资金类业务,那就得换方案,不是这篇文章能覆盖的。

6. 问题排查速查表与几条血泪经验

下面这些是我在开发和上线过程中真实遇到过的问题,按出现频率排序。

6.1 启动期与连接期问题

现象根因处理方式
Cannot determine embedded database driver class排除了自动配置但没提供数据源检查@Configuration是否被扫描到
The connection string contains no value for ...配置用了url而非jdbc-url改字段名,见 2.3 节
The driver could not establish a secure connectionSQL Server 驱动默认encrypt=true加trustServerCertificate=true或配证书
Schema "xxx" does not existPostgreSQL 未指定 schemaURL 加currentSchema=xxx
No suitable driver found驱动包未引入或版本与 JDK 不匹配选.jre11对应的分支
Connection is not available, request timed out连接池耗尽查慢查询、看max-lifetime是否短于对端断连时间

6.2 运行期切换与事务问题

路由失效是最难查的一类,因为代码看着完全正确。我总结了三类常见原因:

一是切面顺序不对。事务切面先执行,切换后执行,结果就是连接早绑好了。表现是"注解写了但查的还是主库"。验证方法很简单:在切面里打一行日志,看它和事务开启日志的先后顺序。

二是自调用导致 AOP 失效。同一个类里 A 方法调 B 方法,B 上的@DS注解不生效,因为走了this调用,没有经过代理。解决方式是拆到两个 Bean,或者注入自身代理。这个问题在新人代码里出现率极高。

三是异步线程丢了上下文。ThreadLocal不会跨线程传递,@Async或者自己提交线程池的任务里,切库注解必然失效。我的处理是显式传参:任务提交时把数据源标识作为参数带过去,在任务体开头set(),结束clear()。用InheritableThreadLocal只能解决新建线程的场景,线程池复用时同样会串。

6.3 几个不太常见但很要命的细节

第一个,max-lifetime和数据库端空闲超时的关系。SQL Server 侧的remote query timeout和网络设备的空闲连接回收策略都可能悄悄掐断连接,如果池子的max-lifetime比它们长,就会出现"取出的连接已经死了"。表现是每天固定时间段偶发报错,重启就好,过一天又犯。解决办法是把max-lifetime设短,并开启connection-test-query。

第二个,PostgreSQL 的stringtype=unspecified。往jsonb字段插字符串时,如果不加这个参数,驱动会把参数当varchar发过去,数据库报类型不匹配。加了之后所有字符串参数都变成unknown类型,可能影响索引使用,所以要谨慎,最好只在特定数据源上开。

第三个,两个库的字符集和排序规则不一致导致的"看起来一样但不相等"。SQL Server 的排序规则如果是SQL_Latin1_General_CP1_CI_AS,比较时不区分大小写和重音;PostgreSQL 的默认排序规则区分大小写。同一批数据在两边查出来的结果数量可能不同。这个问题在数据核对时会被当成"数据丢了",其实只是比较规则不同。

第四个,长时间运行后内存缓慢上涨。如果DataSourceContextHolder用的是ThreadLocal而某个分支漏了clear(),在高并发下会持续持有引用。虽然 key 是弱引用,但 value 不是,配合线程池的长生命周期,泄漏是肯定的。我在 Code Review 里把"每个 set 必须配对 clear"写进了检查清单,用try-finally强制约束。

最后一个经验:上线前一定要做一次"断网演练"。手动把新库的网络断掉,观察主库业务是否受影响。在我的方案里,因为两个连接池完全独立,主库业务不受影响,只是新库相关接口报错。如果用的是共享连接池或者单事务管理器,可能会出现主库接口也跟着超时的情况——这种耦合故障在生产环境里非常致命。

7. 我个人在这套配置上的几点取舍

最后分享几条纯个人偏好,不保证通用,但至少在我这里跑了两年没出大问题。

我坚持不把@DS注解打在 Service 类上,只打在具体方法上。类级别的注解会让整个类的所有方法都切库,包括那些本该查主库的辅助方法,出问题的概率大幅上升。方法级虽然啰嗦,但边界清晰。

我给两个数据源分别配了独立的线程池,不共用。理由是库的响应特性不同,主库快而新库慢,混在一个池里会被慢任务拖垮。线程池隔离之后,新库抖动不会传导到主库业务。

我在切面里加了一行 debug 日志,记录切换前后和当前线程名。这行日志平时不看,但只要出现切库异常,它就是最快的定位手段——直接 grep 线程名,就能看出上下文在哪个环节丢了。

至于后续扩展,如果某天新库的表结构稳定下来、读取量也上来了,我会考虑给它加一层只读从库,把报表类查询分流出去。到那时路由的 key 就不只是库名,还要带上读写标识,AbstractRoutingDataSource的 key 换成组合字符串就行,架子不用动。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询