Spring Boot多数据源配置实战:基于AbstractRoutingDataSource的优雅实现

发布时间:2026/8/14 11:21:03
Spring Boot多数据源配置实战:基于AbstractRoutingDataSource的优雅实现 1. 项目概述为什么需要连接多个数据库在真实的业务开发里单数据库架构往往撑不了多久。我经历过不少项目初期为了图快所有业务表都堆在一个库里。随着业务扩张问题就来了用户中心的读写压力把订单库拖慢运营后台的复杂报表查询影响了核心交易甚至因为一个非核心功能的慢SQL导致整个应用响应延迟。这时候把不同业务的数据拆分到独立的数据库就成了必然选择。Spring Boot实现多数据源连接核心目标就是“优雅”。这里的优雅不是指代码看起来多花哨而是指配置清晰、隔离彻底、切换丝滑、维护简单。你不能让订单查询一不小心跑到了日志库也不能因为加个新数据源就得把老代码翻个底朝天。这背后涉及Spring框架对DataSource、事务管理、ORM框架如MyBatis、JPA的深度整合。接下来我会拆解从设计思路到避坑实操的全过程这套方案经过多个生产环境验证你可以直接拿去用。2. 核心设计思路与方案选型面对多数据源通常有几种路子最原始的是手动获取DataSource高级点用Spring的AbstractRoutingDataSource做动态路由再或者上ShardingSphere这类中间件。对于大多数业务清晰、数据源数量固定比如2-5个的场景我推荐基于AbstractRoutingDataSource配合显式注解的方案。它足够轻量侵入性低并且能与Spring的事务管理完美结合。为什么选这个方案首先它符合Spring的“约定大于配置”哲学。我们通过自定义注解标记方法或类框架在运行时根据注解值自动切换到对应的数据源。其次它与Transactional事务注解可以协同工作这里有个大坑后面细说保证了业务逻辑的原子性。最后它的性能开销极小就是一次ThreadLocal的查找远比代理或拦截整个SQL解析要高效。关键设计考量点数据源隔离每个数据源必须有自己独立的连接池配置如HikariCP、事务管理器、SQL会话工厂如SqlSessionFactory。绝不能混用这是数据混乱的根源。路由粒度我们控制在方法级别。通过自定义DS(“数据源名”)注解可以精确控制每个DAO方法使用哪个库。比在类级别配置更灵活。默认数据源必须指定一个默认数据源。当方法没有标注DS或者某些框架内部初始化逻辑时会使用它避免空指针异常。事务集成这是难点。Spring的事务管理通常绑定一个固定的DataSource。我们需要定制一个支持多数据源的事务管理器确保在带有Transactional的方法中即使内部调用了多个不同DS标注的方法事务也能正确管理通常需要将事务传播行为设置为REQUIRES_NEW或另做处理。3. 详细配置与核心组件实现下面我们一步步实现。假设我们有两个数据库primary主业务库和secondary日志/报表库。3.1 数据源与连接池配置首先在application.yml中配置两个数据源的连接信息。这里以HikariCP为例它是Spring Boot 2.x后的默认连接池性能很好。spring: datasource: primary: jdbc-url: jdbc:mysql://localhost:3306/primary_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: your_password 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://localhost:3307/secondary_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 10 # 报表库可以配置小一点的连接池 minimum-idle: 2注意这里用的是jdbc-url不是url。因为Spring Boot的自动配置可能对url有特殊处理直接使用jdbc-url可以避免一些诡异的配置冲突问题。接着在Java配置类中将这些配置绑定到具体的DataSourceBean上。Configuration public class DataSourceConfig { Bean ConfigurationProperties(prefix spring.datasource.primary) public DataSource primaryDataSource() { // Spring Boot会自动根据配置创建HikariDataSource return DataSourceBuilder.create().build(); } Bean ConfigurationProperties(prefix spring.datasource.secondary) public DataSource secondaryDataSource() { return DataSourceBuilder.create().build(); } }3.2 动态数据源路由核心这是实现优雅切换的核心。我们继承AbstractRoutingDataSource并重写determineCurrentLookupKey方法。它的作用就是当需要获取数据库连接时告诉我该用哪个数据源的key。public class DynamicDataSource extends AbstractRoutingDataSource { /** * 使用ThreadLocal来保存当前线程使用的数据源键值。 * 为什么用ThreadLocal因为Web请求通常是线程隔离的这保证了每个请求的数据源上下文不会互相干扰。 */ private static final ThreadLocalString CONTEXT_HOLDER new ThreadLocal(); /** * 设置当前线程的数据源 */ public static void setDataSourceKey(String dataSourceKey) { CONTEXT_HOLDER.set(dataSourceKey); } /** * 获取当前线程的数据源 */ public static String getDataSourceKey() { return CONTEXT_HOLDER.get(); } /** * 清除当前线程的数据源防止内存泄漏。务必在请求处理完成后清理 */ public static void clearDataSourceKey() { CONTEXT_HOLDER.remove(); } Override protected Object determineCurrentLookupKey() { // 这个方法会被Spring在获取连接时调用 return getDataSourceKey(); } }然后我们需要一个配置类将多个真实的DataSource注入到DynamicDataSource中并指定默认数据源。Configuration EnableTransactionManagement // 启用事务管理 public class DynamicDataSourceConfig { Bean public DataSource dynamicDataSource( Qualifier(primaryDataSource) DataSource primaryDataSource, Qualifier(secondaryDataSource) DataSource secondaryDataSource) { MapObject, Object targetDataSources new HashMap(2); targetDataSources.put(primary, primaryDataSource); targetDataSources.put(secondary, secondaryDataSource); DynamicDataSource dataSource new DynamicDataSource(); // 设置所有目标数据源 dataSource.setTargetDataSources(targetDataSources); // 设置默认数据源 dataSource.setDefaultTargetDataSource(primaryDataSource); // 初始化 dataSource.afterPropertiesSet(); return dataSource; } }3.3 自定义注解与切面编程为了让使用变得简单我们定义一个DS注解。Target({ElementType.TYPE, ElementType.METHOD}) Retention(RetentionPolicy.RUNTIME) Documented public interface DS { String value() default primary; // 默认为主数据源 }最关键的一步创建一个切面AOP在方法执行前根据DS注解的值动态设置数据源键。Aspect Component Order(-1) // 确保该切面在事务切面之前执行非常重要 Slf4j public class DynamicDataSourceAspect { Around(annotation(ds)) public Object around(ProceedingJoinPoint point, DS ds) throws Throwable { String dataSourceKey ds.value(); // 如果当前已经设置了数据源且不是默认的这里可以加日志或做其他处理 if (DynamicDataSource.getDataSourceKey() ! null) { log.debug(当前线程已存在数据源 [{}]将被覆盖为 [{}], DynamicDataSource.getDataSourceKey(), dataSourceKey); } // 设置数据源 DynamicDataSource.setDataSourceKey(dataSourceKey); log.debug(设置数据源为: {}, dataSourceKey); try { // 执行原方法 return point.proceed(); } finally { // 方法执行完毕后清理当前线程的数据源键 DynamicDataSource.clearDataSourceKey(); log.debug(清理数据源); } } }核心要点Order(-1)至关重要。Spring的事务管理也是通过AOP实现的Transactional。我们必须确保数据源切换的切面在事务切面之前执行。因为事务管理器需要在获取连接之前就知道用哪个数据源。如果顺序反了事务管理器会拿到错误或默认的数据源连接导致数据错乱。3.4 MyBatis集成配置如果你用MyBatis需要为每个数据源配置独立的SqlSessionFactory和MapperScannerConfigurer。这里有个技巧将dynamicDataSource作为唯一的数据源注入但通过配置让MyBatis的Mapper接口与特定的SqlSessionFactory绑定。Configuration MapperScan(basePackages com.yourpackage.mapper.primary, sqlSessionFactoryRef primarySqlSessionFactory) public class PrimaryMyBatisConfig { Bean public SqlSessionFactory primarySqlSessionFactory(Qualifier(dynamicDataSource) DataSource dynamicDataSource) throws Exception { SqlSessionFactoryBean sessionFactory new SqlSessionFactoryBean(); sessionFactory.setDataSource(dynamicDataSource); // 设置primary库专用的mapper.xml路径 sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources(classpath:mapper/primary/*.xml)); // 其他配置如类型别名、插件等 return sessionFactory.getObject(); } } Configuration MapperScan(basePackages com.yourpackage.mapper.secondary, sqlSessionFactoryRef secondarySqlSessionFactory) public class SecondaryMyBatisConfig { Bean public SqlSessionFactory secondarySqlSessionFactory(Qualifier(dynamicDataSource) DataSource dynamicDataSource) throws Exception { SqlSessionFactoryBean sessionFactory new SqlSessionFactoryBean(); sessionFactory.setDataSource(dynamicDataSource); sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources(classpath:mapper/secondary/*.xml)); return sessionFactory.getObject(); } }这样com.yourpackage.mapper.primary下的Mapper会自动使用primarySqlSessionFactory而这个工厂虽然也用了dynamicDataSource但通过DS注解在运行时切换最终会路由到正确的物理库。这种设计实现了配置上的解耦。3.5 多数据源事务管理高级话题这是多数据源中最复杂的一环。Spring的Transactional默认只管理一个事务管理器。如果我们有多个数据源就需要多个PlatformTransactionManager。但一个方法上只能指定一个Transactional。方案一链式事务管理器ChainedTransactionManager这是一种折中方案它按照定义的顺序管理多个事务管理器。提交时按正序回滚时按倒序。但它不是分布式事务如XA不能保证绝对的强一致性属于“尽力而为”型。适用于对一致性要求不是100%严苛的跨库操作。Bean public PlatformTransactionManager transactionManager( Qualifier(primaryDataSource) DataSource primaryDataSource, Qualifier(secondaryDataSource) DataSource secondaryDataSource) { // 为每个数据源创建独立的事务管理器 DataSourceTransactionManager primaryTM new DataSourceTransactionManager(primaryDataSource); DataSourceTransactionManager secondaryTM new DataSourceTransactionManager(secondaryDataSource); // 使用链式事务管理器 return new ChainedTransactionManager(primaryTM, secondaryTM); }使用此方案时一个事务方法内操作多个库如果第二个库操作失败第一个库的操作不会回滚。你需要根据业务容忍度来决定是否采用。方案二使用Transactional 手动DS切换并配置事务传播行为更常见的做法是将跨库操作拆分成多个方法每个方法用DS指定数据源并利用Transactional(propagation Propagation.REQUIRES_NEW)为每个方法创建独立的新事务。这样每个库的事务是独立的一个失败不影响另一个。这要求业务上能接受这种中间状态。Service public class OrderService { Transactional public void createOrder(Order order, Log log) { // 主库操作 saveOrder(order); try { // 新开一个事务操作日志库即使失败主库订单已提交 saveLogInNewTransaction(log); } catch (Exception e) { // 记录日志异常但不影响主业务 log.error(记录操作日志失败, e); } } DS(secondary) Transactional(propagation Propagation.REQUIRES_NEW) public void saveLogInNewTransaction(Log log) { logMapper.insert(log); } DS(primary) Transactional(propagation Propagation.REQUIRED) public void saveOrder(Order order) { orderMapper.insert(order); } }4. 使用示例与最佳实践配置好后使用就非常直观了。Repository public interface UserMapper { // 默认使用primary数据源在类上标注或使用默认值 DS(primary) User selectById(Long id); } Repository DS(secondary) // 这个类下所有方法默认使用secondary库 public interface OperationLogMapper { int insert(OperationLog log); DS(primary) // 这个方法可以单独覆盖使用primary库 User selectUserForLog(Long userId); } Service public class BusinessService { Autowired private UserMapper userMapper; Autowired private OperationLogMapper logMapper; public void doBusiness(Long userId) { // 这个方法会使用primary库 User user userMapper.selectById(userId); // 这个方法会使用secondary库类级别注解生效 logMapper.insert(new OperationLog(...)); // 这个方法会使用primary库方法注解覆盖了类注解 User user2 logMapper.selectUserForLog(userId); } }最佳实践与注意事项明确数据源职责规划好每个数据源的用途比如primary用于核心交易secondary用于日志查询third用于风控。并在团队内形成规范避免随意使用。避免在事务中循环切换在一个Transactional方法内部尽量避免多次调用不同DS注解的方法。这可能导致连接持有混乱甚至死锁。如果必须请仔细设计事务传播行为。清理ThreadLocal我们的DynamicDataSourceAspect中使用了finally块进行清理这很重要。如果是在Filter或Interceptor中设置数据源更要确保异常情况下也能清理否则会导致内存泄漏和后续请求数据源错乱。测试要充分多数据源下务必进行单元测试和集成测试验证数据是否真的写入了预期的库。可以写个测试插入数据后分别从两个库查询确认。监控连接池为每个数据源的连接池配置合理的参数如maximumPoolSize并监控活跃连接数、空闲连接数等指标。不同压力的库连接池大小应区别设置。5. 常见问题排查与性能调优在实际使用中你肯定会遇到下面这些问题。5.1 数据源切换失效总是走到默认库可能原因及排查步骤AOP顺序问题这是最常见的原因。确保你的DynamicDataSourceAspect切面使用了Order(-1)或一个较小的值并且生效了。可以在切面方法开始和结束处打日志确认。注解未生效检查DS注解是否被正确扫描。确保切面的Around表达式能匹配到你的方法。对于Spring AOP要关注方法是否是public的因为默认只代理public方法是否在同一个Bean内部调用内部调用不走代理。ThreadLocal污染在异步任务如Async或子线程中ThreadLocal值不会自动传递。你需要手动传递数据源键并在新线程中设置。可以考虑使用TransmittableThreadLocal阿里开源替代ThreadLocal来解决异步上下文传递问题。5.2 事务与数据源切换的冲突现象在Transactional方法中DS注解似乎没起作用所有SQL都跑到了默认数据源。根因Spring事务的代理逻辑在目标方法执行前就获取了数据库连接。如果数据源切换的AOP在事务AOP之后执行那么事务管理器获取连接时ThreadLocal里还没有数据源键自然就用默认数据源了。解决方案确保切面顺序如前所述数据源切面 (Order(-1)) 必须在事务切面之前。如果问题依旧可以尝试将事务管理器也换成我们自定义的、支持动态查找数据源的那种。但更简单的做法是将DS注解加到Service类或方法上而不是Dao/Mapper上。因为事务通常在Service层开启在进入Service方法前就确定数据源能保证事务内连接一致。5.3 性能问题连接池配置不当多数据源意味着多个连接池。配置不当很容易拖慢应用。调优建议参数建议说明maximumPoolSize核心库10-20非核心库5-10不是越大越好。计算公式可参考连接数 ((核心数 * 2) 有效磁盘数)。监控实际使用峰值。minimumIdle设置为maximumPoolSize的1/4到1/2保持一定空闲连接避免突发请求时新建连接的开销。connectionTimeout30000 (30秒)获取连接的超时时间不宜过短。idleTimeout600000 (10分钟)空闲连接存活时间超时后释放。maxLifetime1800000 (30分钟)连接最大生命周期强制刷新避免数据库端连接僵死。监控启用HikariCP的JMX监控或通过/actuator/metrics/hikaricp.connections.*端点如果集成了Spring Boot Actuator查看连接池状态。5.4 在Spring Boot 2.7/3.x中与自动配置的冲突Spring Boot的自动配置非常强大但有时会“多管闲事”。比如它可能会因为你定义了多个DataSourceBean而自动尝试配置一个DataSourceTransactionManager这可能会干扰我们的动态数据源。解决方法 在启动类或主配置类上排除特定的自动配置。SpringBootApplication(exclude { DataSourceAutoConfiguration.class, // 排除数据源自动配置 DataSourceTransactionManagerAutoConfiguration.class, // 排除事务管理器自动配置 JdbcTemplateAutoConfiguration.class // 如果需要也排除JdbcTemplate自动配置 }) public class YourApplication { public static void main(String[] args) { SpringApplication.run(YourApplication.class, args); } }然后完全由我们自己的配置类如DynamicDataSourceConfig来掌控DataSource和TransactionManager的创建。这样能获得最清晰的控制权。5.5 与第三方Starter的兼容性比如你使用了knife4j-spring-boot-starter做接口文档或者spring-boot-starter-data-redis。这些starter有时也会初始化数据源相关的东西。如果遇到奇怪的问题可以查看它们的自动配置类必要时进行排除。一个更通用的原则是对于多数据源这种比较底层的、需要精细控制的配置尽量采用“全手动”模式即排除相关自动配置自己显式定义所有Bean。虽然配置量稍大但避免了无数潜在的、难以调试的隐性冲突。这套基于AbstractRoutingDataSource和自定义注解的方案在我经历过的多个中大型项目中都稳定运行。它提供了足够的灵活性和清晰度。关键在于理解其原理——ThreadLocal上下文传递和AOP拦截顺序并做好事务边界的设计。开始时可能会踩一些坑但一旦跑通后续扩展新的数据源比如加一个tertiary就会非常轻松只需要在配置里加一组数据源定义再增加对应的DS(“tertiary”)注解即可。