Spring+MyBatis多数据源配置实现:从入门到实践

在复杂的业务场景中,多数据源是常见的技术需求:

  • 读写分离:主库(Master)负责写操作,从库(Slave)负责读操作,提升系统吞吐量;
  • 分库分表:将数据分散到多个数据库,解决单库性能瓶颈;
  • 跨系统集成:同一个服务需要访问多个业务线的数据库(如电商系统同时访问订单库和用户库)。

Spring 提供了灵活的数据源管理机制,而 MyBatis 作为优秀的持久层框架,能与 Spring 无缝整合。本文将详细讲解 Spring+MyBatis 多数据源的两种核心实现方式(动态路由/静态绑定),结合最佳实践和完整示例,帮助你快速落地多数据源方案。

目录#

  1. 多数据源基础概念
  2. 前置知识与环境准备
  3. 实现方式一:动态路由(AbstractRoutingDataSource)
    • 3.1 核心原理
    • 3.2 step-by-step 实现
    • 3.3 数据源切换方式(AOP/手动)
  4. 实现方式二:静态绑定(多SqlSessionFactory)
  5. 最佳实践与避坑指南
  6. 常见场景示例:读写分离
  7. 高级话题:动态数据源与分布式事务
  8. 测试策略
  9. 总结
  10. 参考资料

1. 多数据源基础概念#

多数据源指一个应用同时连接多个数据库实例(或 schema)。在 Spring+MyBatis 体系中,核心组件的关系如下:

  • DataSource:数据库连接池(如 HikariCP),是多数据源的基础;
  • SqlSessionFactory:MyBatis 的核心工厂,每个 SqlSessionFactory 绑定一个 DataSource;
  • SqlSession:MyBatis 的会话对象,由 SqlSessionFactory 创建;
  • Mapper:MyBatis 的映射接口,由 SqlSession 生成代理对象。

多数据源的核心问题是如何将不同的数据库操作路由到对应的 DataSource,常见方案分为两类:

  • 动态路由:通过 AbstractRoutingDataSource 动态切换数据源(适合频繁切换场景,如读写分离);
  • 静态绑定:为每个数据源配置独立的 SqlSessionFactoryMapper(适合固定模块绑定固定数据源,如跨系统集成)。

2. 前置知识与环境准备#

2.1 技术栈要求#

  • 基础:Spring Boot、MyBatis、JDBC 核心概念;
  • 工具:Maven/Gradle、IntelliJ IDEA、MySQL(或其他数据库);
  • 依赖:Spring Boot Starter、MyBatis Starter、连接池(HikariCP)、数据库驱动。

2.2 环境搭建#

2.2.1 依赖配置(pom.xml)#

<dependencies>
    <!-- Spring Boot 基础 -->
    <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>
    
    <!-- MyBatis 整合 Spring Boot -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.3.1</version>
    </dependency>
    
    <!-- 数据库驱动(MySQL 为例) -->
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <scope>runtime</scope>
    </dependency>
    
    <!-- 连接池(HikariCP,Spring Boot 2.x 默认) -->
    <dependency>
        <groupId>com.zaxxer</groupId>
        <artifactId>HikariCP</artifactId>
    </dependency>
    
    <!-- 测试 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

2.2.2 配置文件(application.yml)#

假设我们有**主库(Master)从库(Slave)**两个数据源,配置如下:

spring:
  datasource:
    # 主库(写操作)
    master:
      url: jdbc:mysql://localhost:3306/master_db?useSSL=false&serverTimezone=UTC
      username: root
      password: 123456
      driver-class-name: com.mysql.cj.jdbc.Driver
    # 从库(读操作)
    slave:
      url: jdbc:mysql://localhost:3306/slave_db?useSSL=false&serverTimezone=UTC
      username: root
      password: 123456
      driver-class-name: com.mysql.cj.jdbc.Driver
 
# MyBatis 配置
mybatis:
  mapper-locations: classpath:mapper/**/*.xml  # Mapper XML 路径
  type-aliases-package: com.example.demo.entity  # 实体类别名包

3. 实现方式一:动态路由(AbstractRoutingDataSource)#

3.1 核心原理#

AbstractRoutingDataSource 是 Spring 提供的动态数据源路由抽象类,其核心逻辑是:

  1. 维护一个目标数据源映射targetDataSources),键为数据源标识(如 "master"/"slave");
  2. 通过 determineCurrentLookupKey() 方法动态获取当前数据源标识
  3. 将数据库操作转发到对应的目标数据源。

3.2 step-by-step 实现#

3.2.1 定义数据源标识(枚举)#

public enum DataSourceType {
    MASTER, SLAVE
}

3.2.2 线程上下文持有器(ThreadLocal)#

用于存储当前线程的数据源标识(避免线程安全问题):

public class DataSourceContextHolder {
    // ThreadLocal 存储当前线程的数据源类型
    private static final ThreadLocal<DataSourceType> CONTEXT_HOLDER = new ThreadLocal<>();
 
    // 设置数据源类型
    public static void setDataSourceType(DataSourceType type) {
        CONTEXT_HOLDER.set(type);
    }
 
    // 获取数据源类型
    public static DataSourceType getDataSourceType() {
        return CONTEXT_HOLDER.get();
    }
 
    // 清除数据源类型(必须在请求结束时调用,避免内存泄漏)
    public static void clearDataSourceType() {
        CONTEXT_HOLDER.remove();
    }
}

3.2.3 实现动态路由数据源#

继承 AbstractRoutingDataSource,重写 determineCurrentLookupKey() 方法:

public class RoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        // 从上下文持有器中获取当前数据源类型
        return DataSourceContextHolder.getDataSourceType();
    }
}

3.2.4 配置数据源与路由#

创建 Spring 配置类,完成以下步骤:

  1. 配置主库从库DataSource Bean;
  2. 将两个数据源注入 RoutingDataSourcetargetDataSources
  3. 配置 SqlSessionFactory(依赖 RoutingDataSource);
  4. 配置事务管理器(DataSourceTransactionManager)。
@Configuration
@MapperScan(basePackages = "com.example.demo.mapper")  // 扫描所有 Mapper 接口
public class MyBatisConfig {
 
    // 绑定主库配置(对应 application.yml 中的 spring.datasource.master)
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }
 
    // 绑定从库配置
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.slave")
    public DataSource slaveDataSource() {
        return DataSourceBuilder.create().build();
    }
 
    // 配置动态路由数据源
    @Bean
    public DataSource routingDataSource(DataSource masterDataSource, DataSource slaveDataSource) {
        RoutingDataSource routingDataSource = new RoutingDataSource();
        // 目标数据源映射:键为 DataSourceType,值为数据源实例
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DataSourceType.MASTER, masterDataSource);
        targetDataSources.put(DataSourceType.SLAVE, slaveDataSource);
        routingDataSource.setTargetDataSources(targetDataSources);
        // 默认数据源(当无法获取标识时使用)
        routingDataSource.setDefaultTargetDataSource(masterDataSource);
        return routingDataSource;
    }
 
    // 配置 SqlSessionFactory(依赖路由数据源)
    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource routingDataSource) throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(routingDataSource);
        // 配置 MyBatis  mapper 文件路径(与 application.yml 一致)
        factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources("classpath:mapper/**/*.xml"));
        return factoryBean.getObject();
    }
 
    // 配置事务管理器(依赖路由数据源)
    @Bean
    public DataSourceTransactionManager transactionManager(DataSource routingDataSource) {
        return new DataSourceTransactionManager(routingDataSource);
    }
}

3.3 数据源切换方式#

3.3.1 手动切换(基础方式)#

在业务方法中显式设置数据源标识:

@Service
public class UserService {
 
    @Autowired
    private UserMapper userMapper;
 
    // 写操作:使用主库
    @Transactional(rollbackFor = Exception.class)
    public void saveUser(User user) {
        DataSourceContextHolder.setDataSourceType(DataSourceType.MASTER);
        userMapper.insert(user);
        // 清除标识(或在全局拦截器中处理)
        DataSourceContextHolder.clearDataSourceType();
    }
 
    // 读操作:使用从库
    public User getUserById(Long id) {
        DataSourceContextHolder.setDataSourceType(DataSourceType.SLAVE);
        User user = userMapper.selectById(id);
        DataSourceContextHolder.clearDataSourceType();
        return user;
    }
}

3.3.2 AOP 切换(推荐方式)#

手动切换需要重复写 set/clear 代码,通过AOP+自定义注解可以实现无侵入式切换

  1. 定义自定义注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
    DataSourceType value() default DataSourceType.MASTER;
}
  1. 实现 AOP 切面:
@Aspect
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)  // 确保切面优先于事务执行
public class DataSourceAspect {
 
    // 切入点:拦截所有标注 @DataSource 注解的方法
    @Pointcut("@annotation(com.example.demo.annotation.DataSource)")
    public void dataSourcePointCut() {}
 
    // 环绕通知:设置数据源标识
    @Around("dataSourcePointCut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        // 获取方法上的 @DataSource 注解
        DataSource dataSource = method.getAnnotation(DataSource.class);
        if (dataSource != null) {
            // 设置数据源类型
            DataSourceContextHolder.setDataSourceType(dataSource.value());
        }
        try {
            // 执行目标方法
            return joinPoint.proceed();
        } finally {
            // 清除数据源类型(必须执行)
            DataSourceContextHolder.clearDataSourceType();
        }
    }
}
  1. 业务方法中使用注解:
@Service
public class UserService {
 
    @Autowired
    private UserMapper userMapper;
 
    // 写操作:使用主库(默认)
    @Transactional(rollbackFor = Exception.class)
    @DataSource(DataSourceType.MASTER)
    public void saveUser(User user) {
        userMapper.insert(user);
    }
 
    // 读操作:使用从库
    @DataSource(DataSourceType.SLAVE)
    public User getUserById(Long id) {
        return userMapper.selectById(id);
    }
}

4. 实现方式二:静态绑定(多SqlSessionFactory)#

4.1 适用场景#

不同模块的 Mapper 固定绑定不同数据源时(如订单模块用订单库,用户模块用用户库),适合使用静态绑定:为每个数据源配置独立的 SqlSessionFactoryMapper

4.2 step-by-step 实现#

4.2.1 配置多数据源#

假设我们有两个数据源:order_db(订单库)和 user_db(用户库),修改 application.yml

spring:
  datasource:
    order:
      url: jdbc:mysql://localhost:3306/order_db?useSSL=false&serverTimezone=UTC
      username: root
      password: 123456
    user:
      url: jdbc:mysql://localhost:3306/user_db?useSSL=false&serverTimezone=UTC
      username: root
      password: 123456

4.2.2 配置订单库相关 Bean#

@Configuration
// 扫描订单模块的 Mapper(独立包路径)
@MapperScan(basePackages = "com.example.demo.mapper.order", sqlSessionFactoryRef = "orderSqlSessionFactory")
public class OrderDataSourceConfig {
 
    // 订单库 DataSource
    @Bean(name = "orderDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.order")
    public DataSource orderDataSource() {
        return DataSourceBuilder.create().build();
    }
 
    // 订单库 SqlSessionFactory
    @Bean(name = "orderSqlSessionFactory")
    public SqlSessionFactory orderSqlSessionFactory(@Qualifier("orderDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
        factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources("classpath:mapper/order/**/*.xml"));  // 订单 Mapper XML 路径
        return factoryBean.getObject();
    }
 
    // 订单库 SqlSessionTemplate
    @Bean(name = "orderSqlSessionTemplate")
    public SqlSessionTemplate orderSqlSessionTemplate(@Qualifier("orderSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }
 
    // 订单库事务管理器
    @Bean(name = "orderTransactionManager")
    public DataSourceTransactionManager orderTransactionManager(@Qualifier("orderDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

4.2.3 配置用户库相关 Bean#

@Configuration
@MapperScan(basePackages = "com.example.demo.mapper.user", sqlSessionFactoryRef = "userSqlSessionFactory")
public class UserDataSourceConfig {
 
    @Bean(name = "userDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.user")
    public DataSource userDataSource() {
        return DataSourceBuilder.create().build();
    }
 
    @Bean(name = "userSqlSessionFactory")
    public SqlSessionFactory userSqlSessionFactory(@Qualifier("userDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
        factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources("classpath:mapper/user/**/*.xml"));
        return factoryBean.getObject();
    }
 
    @Bean(name = "userSqlSessionTemplate")
    public SqlSessionTemplate userSqlSessionTemplate(@Qualifier("userSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }
 
    @Bean(name = "userTransactionManager")
    public DataSourceTransactionManager userTransactionManager(@Qualifier("userDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

4.2.4 业务使用#

@Service
public class OrderService {
 
    // 自动注入订单库 Mapper(由 @MapperScan 扫描)
    @Autowired
    private OrderMapper orderMapper;
 
    // 自动注入用户库 Mapper
    @Autowired
    private UserMapper userMapper;
 
    // 订单库事务:使用 orderTransactionManager
    @Transactional(transactionManager = "orderTransactionManager", rollbackFor = Exception.class)
    public void createOrder(Order order) {
        orderMapper.insert(order);
    }
 
    // 用户库查询:使用 userSqlSessionTemplate
    public User getUserById(Long userId) {
        return userMapper.selectById(userId);
    }
}

5. 最佳实践与避坑指南#

5.1 必遵循的最佳实践#

  1. 使用连接池:优先选择 HikariCP(Spring Boot 2.x 默认,性能最优),配置合理的池大小(如 minimum-idle: 5maximum-pool-size: 20);
  2. 清除 ThreadLocal:使用 DataSourceContextHolder 时,必须在请求结束或方法返回时清除(否则会导致内存泄漏);
  3. 事务与数据源一致性:事务方法中不要切换数据源(否则事务会失效,因为事务绑定的是初始数据源);
  4. AOP 优先级:确保数据源切换的 AOP 切面优先级高于事务切面@Order(Ordered.HIGHEST_PRECEDENCE));
  5. 监控与报警:使用 Spring Boot Actuator 监控数据源连接池状态(如 spring.boot.actuator.endpoints.web.exposure.include=health,metrics)。

5.2 常见坑点与解决#

问题原因解决
线程安全问题未使用 ThreadLocal 存储数据源标识必须用 ThreadLocal 持有数据源标识
内存泄漏未清除 ThreadLocal 中的值finally 块或全局拦截器中调用 clear()
事务失效事务方法中切换数据源事务开始前切换数据源,或使用分布式事务
数据源未找到targetDataSources 未配置对应键检查 DataSourceTypetargetDataSources 的键是否一致

6. 常见场景示例:读写分离#

6.1 场景描述#

  • 主库(Master):处理写操作(INSERT/UPDATE/DELETE);
  • 从库(Slave):处理读操作(SELECT);
  • 要求:写操作自动路由到主库,读操作自动路由到从库。

6.2 实现步骤#

  1. 3.2 节 配置动态路由数据源;
  2. 使用 3.3.2 节 的 AOP+注解方案;
  3. 所有读方法添加 @DataSource(DataSourceType.SLAVE)写方法使用默认主库。

6.3 验证#

  • 写操作:调用 saveUser(),检查主库是否插入数据;
  • 读操作:调用 getUserById(),检查从库是否查询到数据(需确保主从同步正常)。

7. 高级话题#

7.1 动态数据源注册( runtime 新增)#

如果需要在运行时动态添加数据源(如 SaaS 多租户场景),可以通过 Spring 的 BeanDefinitionRegistry 实现:

@Service
public class DynamicDataSourceService {
 
    @Autowired
    private ApplicationContext applicationContext;
 
    // 动态注册数据源
    public void registerDataSource(String dataSourceName, DataSourceProperties properties) {
        // 创建 DataSource BeanDefinition
        BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(HikariDataSource.class);
        builder.addPropertyValue("jdbcUrl", properties.getUrl());
        builder.addPropertyValue("username", properties.getUsername());
        builder.addPropertyValue("password", properties.getPassword());
        builder.addPropertyValue("driverClassName", properties.getDriverClassName());
 
        // 注册 Bean
        BeanDefinitionRegistry registry = (BeanDefinitionRegistry) applicationContext.getAutowireCapableBeanFactory();
        registry.registerBeanDefinition(dataSourceName + "DataSource", builder.getBeanDefinition());
 
        // 更新 RoutingDataSource 的 targetDataSources
        RoutingDataSource routingDataSource = (RoutingDataSource) applicationContext.getBean("routingDataSource");
        Map<Object, Object> targetDataSources = new HashMap<>(routingDataSource.getTargetDataSources());
        targetDataSources.put(dataSourceName, applicationContext.getBean(dataSourceName + "DataSource"));
        routingDataSource.setTargetDataSources(targetDataSources);
        routingDataSource.afterPropertiesSet();  // 重新初始化
    }
}

7.2 MyBatis Plus 多数据源(简化方案)#

MyBatis Plus 提供了动态数据源 startermybatis-plus-boot-starter-dynamic-datasource),可以零配置实现多数据源

  1. 添加依赖:
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
    <version>3.5.3</version>
</dependency>
  1. 配置多数据源:
spring:
  datasource:
    dynamic:
      primary: master  # 默认数据源
      datasource:
        master:
          url: jdbc:mysql://localhost:3306/master_db
          username: root
          password: 123456
        slave:
          url: jdbc:mysql://localhost:3306/slave_db
          username: root
          password: 123456
  1. 业务使用(@DS 注解):
@Service
public class UserService {
 
    @Autowired
    private UserMapper userMapper;
 
    @DS("master")  // 写操作:主库
    @Transactional
    public void saveUser(User user) {
        userMapper.insert(user);
    }
 
    @DS("slave")  // 读操作:从库
    public User getUserById(Long id) {
        return userMapper.selectById(id);
    }
}

7.3 分布式事务处理#

当业务需要跨多个数据源执行事务(如同时修改订单库和用户库),单数据源事务管理器(DataSourceTransactionManager)无法满足需求,需使用分布式事务框架

  • Seata:阿里开源的高性能分布式事务解决方案,支持 AT(自动补偿)、TCC 模式;
  • Atomikos:开源的 JTA 事务管理器,支持 XA 协议。

8. 测试策略#

8.1 单元测试(验证数据源切换)#

@SpringBootTest
class UserServiceTest {
 
    @Autowired
    private UserService userService;
 
    @Autowired
    private DataSource routingDataSource;
 
    @Test
    void testSaveUser() {
        // 1. 调用写方法(主库)
        User user = new User();
        user.setName("test");
        userService.saveUser(user);
 
        // 2. 验证数据源是否为 MASTER
        DataSourceType currentType = DataSourceContextHolder.getDataSourceType();
        Assertions.assertEquals(DataSourceType.MASTER, currentType);
    }
 
    @Test
    void testGetUserById() {
        // 1. 调用读方法(从库)
        User user = userService.getUserById(1L);
 
        // 2. 验证数据源是否为 SLAVE
        DataSourceType currentType = DataSourceContextHolder.getDataSourceType();
        Assertions.assertEquals(DataSourceType.SLAVE, currentType);
    }
}

8.2 集成测试(验证数据一致性)#

  • 写操作:调用 saveUser() 后,检查主库是否存在数据;
  • 读操作:调用 getUserById() 后,检查从库是否同步到数据(需主从复制正常)。

9. 总结#

  • 动态路由(AbstractRoutingDataSource):适合频繁切换数据源(如读写分离),通过 ThreadLocal+AOP 实现无侵入切换;
  • 静态绑定(多 SqlSessionFactory):适合固定模块绑定固定数据源(如跨系统集成),配置独立的 Mapper 和事务管理器;
  • 最佳实践:优先使用连接池、清除 ThreadLocal、确保事务与数据源一致;
  • 高级扩展:MyBatis Plus 简化多数据源配置,Seata 解决分布式事务问题。

10. 参考资料#

  1. Spring 官方文档:AbstractRoutingDataSource
  2. MyBatis 官方文档:Spring Integration
  3. MyBatis Plus 动态数据源:Dynamic Datasource
  4. Seata 官方文档:Seata 快速开始
  5. 《Spring Boot 实战》:第 6 章 数据访问。