☰
Spring Boot + MyBatis 多数据源配置实战:读写分离与动态路由
2026/10/19 11:39:26 网站建设 项目流程

1. 多数据源到底在解决什么问题

第一次接触多数据源配置的人,脑子里往往有个疑问:我一个项目连一个数据库好好的,为什么非要折腾多个数据源?这个问题不搞清楚,后面配出来的东西就是照猫画虎,出了问题也不知道从哪查。

先说最典型的场景:读写分离。业务系统跑着跑着,数据库压力上来了,读请求远多于写请求,比例可能到 8:1 甚至更高。这时候常规做法是搞一主多从,主库负责写,从库负责读,把查询流量分摊出去。但代码层面怎么区分?总不能在每个 Service 里手动判断“这个方法该走主库还是从库”吧。多数据源配置配合 AOP 切面,就能做到对业务代码几乎无侵入——加个注解,框架自动帮你路由。

另一个场景是多库共存。比如订单库和用户库物理隔离,或者老系统迁移过程中新旧库并行,再或者要同时访问业务库和日志库。这些情况下,一个数据源根本不够用,必须让 Spring 容器里存在多个 DataSource Bean,并且能按需切换。

还有一个容易被忽略的场景:分库分表后的统一入口。虽然严格来说分库分表中间件会接管路由,但在一些轻量级方案里,开发者会自己配置多个数据源,在应用层做简单的分片逻辑。

所以多数据源的核心诉求就一句话:让不同的数据库操作,走不同的数据库连接。听起来简单,但 Spring 和 MyBatis 的默认行为是“一个 SqlSessionFactory 绑定一个 DataSource”,要打破这个默认,就得手动接管一些 Bean 的创建过程。

我见过不少项目在这块翻车,根本原因不是配置写错了,而是没理解 Spring 自动配置的加载顺序和 Bean 覆盖机制。下面我会把整个思路拆开,从原理到代码,一步步说清楚。

1.1 读写分离的本质与常见误区

读写分离的本质是流量分离,不是数据分离。主库和从库的数据最终是一致的(通过数据库层面的复制机制),应用层只是决定“这次操作去哪边拿连接”。

这里有个误区必须提前说清楚:读写分离不等于读写一致性。主库写入后,从库同步有延迟,可能是毫秒级,也可能是秒级。如果你在写完之后立刻去从库读,很可能读到旧数据。这个问题在代码层面无法根治,只能通过业务设计规避——比如写后立即读的场景强制走主库,或者接受最终一致性。

另一个误区是所有读操作都走从库。实际上,一些对实时性要求高的查询,比如支付状态确认、库存扣减前的校验,必须走主库。所以路由规则不能简单按“读/写”二分,要留出强制走主库的口子。

我在实际项目中见过最粗暴的做法是:在方法名上做文章,select开头的走从库,insert/update/delete开头的走主库。这种做法在简单项目里能跑,但一旦遇到复杂查询或者存储过程就歇菜了。更靠谱的方式是用注解显式声明,默认走主库,需要走从库的地方手动标注。

1.2 为什么选择 MyBatis 而非 JPA 做多数据源

Spring Boot 生态里,持久层选型主要是 MyBatis 和 JPA 两大阵营。做多数据源配置时,MyBatis 的优势非常明显:SQL 可控。

JPA 的 EntityManager 和 DataSource 绑定较深,多数据源配置需要处理多个 EntityManagerFactory,事务管理器的配置也更复杂。而 MyBatis 的核心是 SqlSessionFactory,它依赖 DataSource 和 Mapper 扫描配置,结构清晰,手动创建多个实例相对容易。

更重要的是,MyBatis 的 Mapper 接口和 XML 文件是显式绑定的,你可以精确控制哪个 Mapper 走哪个 SqlSessionFactory。JPA 的 Repository 虽然也能指定,但底层 EntityManager 的切换容易出幺蛾子。

所以如果你的项目是 MyBatis 技术栈,多数据源配置其实是一件很“直白”的事情——创建多个 DataSource,创建多个 SqlSessionFactory,把 Mapper 分别注册进去,最后用 AOP 或注解做路由。下面我会按这个脉络展开。

2. 核心配置思路与方案选型

动手写代码之前,先把整体架构想清楚。多数据源配置不是堆砌 Bean,而是要有清晰的分层:数据源层、会话工厂层、路由层、事务层。每一层各司其职,才能保证整个链路稳定。

2.1 整体架构分层设计

数据源层负责管理多个 DataSource 实例。每个数据源对应一个数据库连接池,拥有独立的连接参数、池化配置。这一层的关键是给每个 DataSource 起一个唯一的名字,后续所有引用都靠这个名字。

会话工厂层为每个数据源创建独立的 SqlSessionFactory。MyBatis 的 SqlSessionFactory 是重量级对象,创建后应该单例复用。每个工厂绑定自己的 DataSource 和 Mapper 扫描路径,互不干扰。

路由层决定一次数据库操作走哪个数据源。实现方式有两种:一是用 Spring 的 AbstractRoutingDataSource 做动态路由,二是用 AOP 在方法调用前手动切换。前者更优雅,后者更直观。

事务层是最容易出问题的地方。多数据源下,事务管理器必须和具体的数据源绑定。如果你在主库上开了事务,然后去从库查询,这个查询不会在主库事务里,回滚也管不到它。所以事务管理器的配置必须和数据源路由策略对齐。

我推荐的方案是:静态多数据源 + 注解路由。也就是说,数据源在启动时就确定好,不在运行时动态增删;路由通过自定义注解 + AOP 实现,默认走主库,需要走从库的方法显式标注。这个方案的好处是结构简单、排查容易、事务边界清晰。

2.2 动态路由与静态配置的取舍

AbstractRoutingDataSource 是 Spring 提供的一个抽象类,核心方法是determineCurrentLookupKey(),返回一个 key,框架根据这个 key 从目标数据源 Map 里找对应的 DataSource。这个方案适合数据源数量不确定、需要运行时切换的场景。

但在读写分离场景下,数据源是固定的(一个主库、N 个从库),用 AbstractRoutingDataSource 反而增加了复杂度。因为你要维护一个 ThreadLocal 来存当前线程的 key,还要考虑线程池复用时的清理问题,稍不注意就会串数据源。

静态配置方案则是:主库一个 DataSource,从库一个 DataSource,两个 SqlSessionFactory,两套 Mapper 扫描。需要走从库的 Mapper 单独放在一个包下,或者用注解在方法级别切换。这个方案的问题是 Mapper 要分开写,不能同一个 Mapper 里既有走主库的方法又有走从库的方法。

折中方案是:一个 DataSource 用 AbstractRoutingDataSource 包装,内部持有主库和从库两个真实 DataSource,通过 AOP 在方法调用前设置路由 key。这样 Mapper 不用拆包,事务管理器也只需要一个。但要注意,事务开启后不能再切换数据源,否则事务会失效。

我个人的经验是:如果读写分离的规则比较简单(比如只有几个查询走从库),用静态配置最省心;如果规则复杂、需要动态调整,再上 AbstractRoutingDataSource。

2.3 事务管理器的绑定策略

事务管理器必须绑定 DataSource。在单数据源项目里,Spring Boot 自动配置一个 DataSourceTransactionManager,你什么都不用管。多数据源下,自动配置失效,你需要手动创建。

关键点:一个事务管理器只能管一个数据源。如果你配置了两个 DataSource,就需要两个事务管理器,分别绑定。然后在@Transactional注解里指定用哪个事务管理器。

但读写分离场景下,写操作走主库,读操作走从库,事务通常只发生在写操作上。所以可以只为主库配置事务管理器,从库的查询不开事务。这样既简化了配置,也符合业务逻辑——读操作本来就不需要事务。

如果你确实需要跨数据源的事务(比如同时写两个库),那就涉及到分布式事务了,这超出了本文的范围。简单说,单机多数据源的事务一致性,靠本地事务管理器是保证不了的,需要引入额外的协调机制。

3. 手把手配置多数据源

理论说完了,开始动手。我会用一个模拟项目来演示,项目里有两个数据库:一个主库(写),一个从库(读)。技术栈是 Spring Boot + MyBatis + HikariCP 连接池。

3.1 依赖引入与项目结构

依赖方面,核心是这几个:

<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.zaxxer</groupId> <artifactId>HikariCP</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency>

注意:引入mybatis-spring-boot-starter后,Spring Boot 会自动配置一个 DataSource 和一个 SqlSessionFactory。多数据源配置的第一步,就是排除这个自动配置,否则会和你手动创建的 Bean 冲突。

排除方式有两种:一是在启动类上加@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}),二是在配置文件里设置spring.autoconfigure.exclude。我推荐第一种,更直观。

项目结构建议这样组织:

src/main/java/com/example/project/ ├── config/ │ ├── PrimaryDataSourceConfig.java │ └── SecondaryDataSourceConfig.java ├── annotation/ │ └── ReadOnly.java ├── aspect/ │ └── DataSourceAspect.java ├── context/ │ └── DataSourceContextHolder.java ├── mapper/ │ ├── primary/ │ │ └── OrderMapper.java │ └── secondary/ │ └── UserMapper.java └── service/ └── OrderService.java

Mapper 按数据源分包存放,这样在配置 SqlSessionFactory 时可以直接指定扫描路径,避免混淆。

3.2 主从数据源 Bean 的创建

先看配置文件application.yml:

spring: datasource: primary: jdbc-url: jdbc:mysql://127.0.0.1:3306/main_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: main_user password: main_pass driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000 secondary: jdbc-url: jdbc:mysql://127.0.0.1:3307:3306/slave_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: slave_user password: slave_pass driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 30 minimum-idle: 10 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000

注意这里用了jdbc-url而不是url,因为 HikariCP 的配置属性名是jdbcUrl,用url会报错。这是很多人第一次配置时踩的坑。

主数据源配置类:

@Configuration @MapperScan(basePackages = "com.example.project.mapper.primary", sqlSessionFactoryRef = "primarySqlSessionFactory") public class PrimaryDataSourceConfig { @Primary @Bean(name = "primaryDataSource") @ConfigurationProperties(prefix = "spring.datasource.primary") public DataSource primaryDataSource() { return DataSourceBuilder.create().type(HikariDataSource.class).build(); } @Primary @Bean(name = "primarySqlSessionFactory") public SqlSessionFactory primarySqlSessionFactory( @Qualifier("primaryDataSource") DataSource dataSource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); bean.setMapperLocations( new PathMatchingResourcePatternResolver() .getResources("classpath:mapper/primary/*.xml")); org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration(); configuration.setMapUnderscoreToCamelCase(true); configuration.setCacheEnabled(false); bean.setConfiguration(configuration); return bean.getObject(); } @Primary @Bean(name = "primaryTransactionManager") public DataSourceTransactionManager primaryTransactionManager( @Qualifier("primaryDataSource") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } }

从数据源配置类结构类似,只是把@Primary去掉,Bean 名字改成 secondary 相关:

@Configuration @MapperScan(basePackages = "com.example.project.mapper.secondary", sqlSessionFactoryRef = "secondarySqlSessionFactory") public class SecondaryDataSourceConfig { @Bean(name = "secondaryDataSource") @ConfigurationProperties(prefix = "spring.datasource.secondary") 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/secondary/*.xml")); org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration(); configuration.setMapUnderscoreToCamelCase(true); configuration.setCacheEnabled(false); bean.setConfiguration(configuration); return bean.getObject(); } @Bean(name = "secondaryTransactionManager") public DataSourceTransactionManager secondaryTransactionManager( @Qualifier("secondaryDataSource") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } }

这里有几个关键点需要解释:

@Primary的作用:当 Spring 容器里存在多个同类型 Bean 时,@Primary标记的那个会被优先注入。主数据源加@Primary,是为了让那些没有显式指定数据源的自动配置(比如 Spring Boot 的 JdbcTemplate 自动配置)默认使用主库。

@MapperScan的sqlSessionFactoryRef:这个属性指定该包下的 Mapper 接口使用哪个 SqlSessionFactory。如果不指定,MyBatis 会尝试自动注入,多数据源下会报“expected single matching bean but found 2”的错误。

@ConfigurationProperties的 prefix:这个注解把配置文件里对应前缀的属性绑定到 DataSource 上。HikariCP 的属性(如 maximum-pool-size)会自动映射到 HikariDataSource 的对应字段。

Mapper XML 的路径:主库和从库的 XML 文件放在不同目录下,避免扫描时混淆。如果你的项目用纯注解方式写 SQL,可以省略setMapperLocations,但 Mapper 接口的包路径还是要分开。

3.3 Mapper 接口与 XML 的归属划分

Mapper 的划分是多数据源配置里最需要规划的部分。我的建议是按数据源分包,而不是按业务模块分包。因为 SqlSessionFactory 的扫描路径是按包配置的,如果同一个包里既有主库 Mapper 又有从库 Mapper,就没法用@MapperScan自动注册了。

主库 Mapper 示例:

package com.example.project.mapper.primary; @Mapper public interface OrderMapper { int insertOrder(Order order); int updateOrderStatus(@Param("orderId") Long orderId, @Param("status") Integer status); Order selectByOrderId(@Param("orderId") Long orderId); }

从库 Mapper 示例:

package com.example.project.mapper.secondary; @Mapper public interface UserMapper { User selectByUserId(@Param("userId") Long userId); List<User> selectByCondition(UserQuery query); }

对应的 XML 文件分别放在resources/mapper/primary/和resources/mapper/secondary/下。XML 里的 namespace 要写对应的 Mapper 接口全限定名。

这里有个细节:如果两个数据源的 Mapper 接口有同名方法,不会冲突,因为它们绑定在不同的 SqlSessionFactory 上。但如果两个 XML 文件里有相同的 namespace 和 statement id,就会报错。所以 XML 文件的命名也要区分开。

3.4 用注解 + AOP 实现读写路由

静态分包的方式有个局限:同一个 Mapper 里的方法不能灵活切换数据源。比如 OrderMapper 里有个查询方法,你希望它走从库,但其他方法走主库。这时候就需要注解 + AOP 来做方法级别的路由。

先定义注解:

@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface ReadOnly { }

再定义 ThreadLocal 上下文:

public class DataSourceContextHolder { private static final ThreadLocal<String> 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(); } }

AOP 切面:

@Aspect @Component @Order(1) public class DataSourceAspect { @Around("@annotation(readOnly)") public Object around(ProceedingJoinPoint point, ReadOnly readOnly) throws Throwable { try { DataSourceContextHolder.setDataSource("secondary"); return point.proceed(); } finally { DataSourceContextHolder.clear(); } } }

然后需要一个动态数据源类,继承 AbstractRoutingDataSource:

public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DataSourceContextHolder.getDataSource(); } }

在配置类里,把主库和从库包装进 DynamicDataSource:

@Bean(name = "dynamicDataSource") @Primary public DataSource dynamicDataSource( @Qualifier("primaryDataSource") DataSource primary, @Qualifier("secondaryDataSource") DataSource secondary) { Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put("primary", primary); targetDataSources.put("secondary", secondary); DynamicDataSource dynamicDataSource = new DynamicDataSource(); dynamicDataSource.setTargetDataSources(targetDataSources); dynamicDataSource.setDefaultTargetDataSource(primary); return dynamicDataSource; }

这样,没有@ReadOnly注解的方法默认走主库,加了注解的走从库。注意@Order(1)要加在切面上,保证它在事务切面之前执行,否则事务开启后再切换数据源会失效。

注意:AOP 切面的执行顺序非常关键。事务切面的默认 order 是Ordered.LOWEST_PRECEDENCE,也就是最低优先级。如果你的数据源切面 order 比它大,就会在事务开启后才切换数据源,导致切换无效。所以数据源切面的 order 要设成比事务切面小,比如 1。

4. 常见问题与排查技巧实录

配置多数据源的过程中,报错是家常便饭。我把这些年遇到的高频问题整理成一张速查表,方便你对照排查。

4.1 启动报错与 Bean 冲突速查

报错信息原因解决方法
expected single matching bean but found 2容器里有多个同类型 Bean,注入时没指定用@Qualifier指定 Bean 名字,或给主 Bean 加@Primary
DataSource not found自动配置被排除后,没有手动创建 DataSource检查配置类是否被扫描到,@Configuration是否生效
jdbcUrl is required with driverClassName配置属性名写成了url而不是jdbc-url改用jdbc-url,或自定义 DataSource 的绑定方式
Invalid bound statement (not found)Mapper XML 路径没配对,或 namespace 写错检查setMapperLocations的路径和 XML 的 namespace
No qualifying bean of type 'SqlSessionFactory'@MapperScan没指定sqlSessionFactoryRef在每个@MapperScan上显式指定对应的工厂 Bean 名

这张表里的问题,我几乎每个都踩过。特别是第一个和最后一个,在多数据源配置里出现频率极高。核心原因就是 Spring 的自动注入规则是“按类型找 Bean”,当同类型 Bean 多于一个时,它不知道选哪个,就报错了。

4.2 事务不生效与数据源串线排查

事务不生效是多数据源下最隐蔽的问题。表现是:明明加了@Transactional,但回滚没起作用,或者数据写到了错误的数据源。

原因一:事务管理器绑定了错误的数据源。如果你在@Transactional里没指定transactionManager,Spring 会用默认的。多数据源下默认事务管理器可能绑定的是从库,而你的写操作在主库,事务自然管不到。

解决方法:在@Transactional注解里显式指定事务管理器:

@Transactional(transactionManager = "primaryTransactionManager") public void createOrder(Order order) { orderMapper.insertOrder(order); }

原因二:数据源切换发生在事务开启之后。前面说过,AOP 切面的 order 要小于事务切面。如果顺序反了,事务已经绑定到主库连接,你再切换到从库,这个切换不会生效,因为事务里的连接已经固定了。

原因三:ThreadLocal 没清理导致串线。如果用了线程池,一个线程处理完请求后,ThreadLocal 里的数据源 key 没被清除,下一个请求复用这个线程时,就会用到错误的数据源。所以 AOP 的finally块里一定要调用clear()。

排查这类问题的技巧:在determineCurrentLookupKey()方法里打日志,输出当前线程名和 key 值。然后观察日志,看每次数据库操作时 key 是否符合预期。如果发现同一个线程在不同请求里 key 没变,那就是 ThreadLocal 没清理。

4.3 连接池参数调优与监控

多数据源意味着多个连接池,每个池的参数都要单独配置。如果两个池都用默认值,可能出现主库连接不够用、从库连接闲置的情况。

主库的连接池建议配置:

  • maximum-pool-size:根据写入并发量设置,一般 10-20 足够。写操作通常比读操作慢,连接持有时间长,池子太大会导致数据库压力过大。
  • minimum-idle:设成最大连接数的一半左右,避免频繁创建销毁连接。
  • connection-timeout:30 秒,超过这个时间拿不到连接就报错,避免请求无限等待。
  • max-lifetime:1800 秒,比数据库的wait_timeout小一点,防止连接被数据库单方面断开。

从库的连接池可以配大一些,因为读操作并发高、执行快:

  • maximum-pool-size:20-30,根据从库数量和查询并发调整。
  • minimum-idle:10 左右,保持一定数量的热连接。

监控方面,HikariCP 提供了 JMX 支持,可以通过 JMX 客户端查看连接池的活跃连接数、空闲连接数、等待线程数等指标。如果发现等待线程数持续大于 0,说明连接池太小,需要调大maximum-pool-size。

提示:连接池不是越大越好。数据库的 max_connections 是有限的,如果每个应用实例都配很大的池,总连接数会超过数据库上限,导致连接被拒绝。计算方式是:单实例池大小 × 实例数 < 数据库 max_connections。

4.4 多数据源下的分页与缓存陷阱

MyBatis 的分页插件(如 PageHelper)在多数据源下需要特别注意。PageHelper 的工作原理是在 SQL 执行前拦截,自动加上 limit 语句。它依赖SqlSessionFactory的配置,如果多个数据源共用一个分页插件实例,可能出现分页参数串线的问题。

解决方法:为每个 SqlSessionFactory 单独配置分页插件实例,不要用@Bean单例注入。或者用 MyBatis-Plus 的分页插件,它在配置上更灵活。

缓存方面,MyBatis 的一级缓存是 SqlSession 级别的,多数据源下每个 SqlSession 独立,不会互相影响。但二级缓存是 Mapper 级别的,如果两个数据源的 Mapper 有相同的 namespace,二级缓存会冲突。所以我在配置里把cacheEnabled设成了 false,避免这个问题。如果确实需要二级缓存,确保每个 Mapper 的 namespace 全局唯一。

还有一个容易忽略的点:从库查询的结果可能被缓存,导致读到旧数据。如果从库有延迟,而你又在应用层做了缓存,那延迟会被放大。所以读写分离场景下,缓存策略要更保守,或者对实时性要求高的查询强制走主库。

5. 一些实战中的经验补充

配置跑通只是第一步,真正上线后还会遇到各种边界情况。这部分我分享几个实际项目中总结的经验,都是文档里不会写的。

5.1 从库延迟的应对策略

从库延迟是读写分离的固有缺陷,无法消除,只能缓解。我的做法是:在写操作后的一段时间内,强制读主库。

具体实现:在写入方法里,往 ThreadLocal 里放一个时间戳,标记“该用户最近有写操作”。然后在读方法的路由逻辑里判断,如果当前时间距离上次写操作小于某个阈值(比如 500 毫秒),就走主库。

这个方案需要在网关层或用户会话层维护状态,实现起来有一定复杂度。更简单的做法是:对实时性要求高的查询,直接在 Mapper 方法上加@ReadOnly的反向注解,强制走主库。虽然粗暴,但有效。

还有一种方案是用数据库的 GTID 或位点信息来判断从库是否同步完成,但这需要数据库层面的支持,应用层实现起来更复杂。

5.2 多数据源配置的代码组织建议

多数据源配置类容易写得又臭又长,因为每个数据源都要重复一遍 DataSource、SqlSessionFactory、TransactionManager 的创建逻辑。我的建议是抽一个抽象基类,把公共逻辑放进去,子类只需要提供配置前缀和 Mapper 扫描路径。

public abstract class BaseDataSourceConfig { protected SqlSessionFactory buildSqlSessionFactory( DataSource dataSource, String mapperLocation) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); bean.setMapperLocations( new PathMatchingResourcePatternResolver() .getResources(mapperLocation)); org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration(); configuration.setMapUnderscoreToCamelCase(true); configuration.setCacheEnabled(false); bean.setConfiguration(configuration); return bean.getObject(); } }

这样每个子类只需要十几行代码,维护起来清爽很多。如果数据源数量超过三个,这个抽象基类几乎是必须的。

5.3 上线前的检查清单

多数据源配置上线前,我通常会过一遍这个清单:

  • 主数据源是否加了@Primary,确保默认注入正确
  • 每个@MapperScan是否指定了sqlSessionFactoryRef
  • 事务管理器是否和写操作的数据源绑定
  • AOP 切面的 order 是否小于事务切面的 order
  • ThreadLocal 是否在 finally 里清理
  • 连接池参数是否根据数据库上限做了计算
  • 从库延迟是否有业务层面的兜底方案
  • 分页插件是否为每个 SqlSessionFactory 单独配置

这个清单看起来简单,但每一条都对应一个真实的坑。我见过项目上线后才发现事务没生效,数据写了一半回滚不了;也见过从库延迟导致用户下单后查不到订单,客服电话被打爆。多数据源配置不难,难的是把边界情况都考虑到。

5.4 后续扩展方向

如果项目规模继续扩大,多数据源配置还可以往几个方向扩展。

一是动态数据源注册。结合配置中心,在运行时动态添加或移除数据源,适合多租户场景。每个租户一个数据库,租户注册时动态创建 DataSource 并注册到 DynamicDataSource 里。

二是数据库健康检查与自动降级。定时检测从库的可用性,如果从库挂了,自动把读流量切回主库,避免请求失败。这个需要配合监控和告警一起做。

三是分库分表中间件集成。当单库单表也扛不住时,就需要引入分库分表方案。这时候多数据源配置会变成分片数据源的配置,路由逻辑从“读写分离”变成“分片键路由”,复杂度会上一个台阶。

我个人在实际操作中的体会是:多数据源配置的核心不是代码写得多漂亮,而是把数据流向想清楚。哪个操作走哪个库,事务边界在哪里,延迟怎么处理,这些问题在写代码之前就要有答案。代码只是把这些答案翻译成 Spring 的 Bean 配置而已。想清楚了,配置就是水到渠成的事;没想清楚,配出来的东西迟早要返工。

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

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

立即咨询