4-3 多数据源配置

3 阅读3分钟

4-3 多数据源配置

概念解析

何时需要多数据源

场景说明
主从分离读/写分离,提高性能
分库分表数据量大的表拆分
多业务隔离不同业务使用不同数据库
历史数据归档冷热数据分离

配置方式

方式复杂度适用场景
Spring 原生简单多数据源
Druid 多数据源大部分场景
Seata AT 模式分布式事务

代码示例

1. 基础多数据源配置

@Configuration
public class DataSourceConfig {

    // 主数据源
    @Bean
    @Primary  // 默认数据源
    @ConfigurationProperties(prefix = "spring.datasource.primary")
    public DataSource primaryDataSource() {
        return DruidDataSourceFactory.createDataSource(
            new DruidConfig());
    }

    // 从数据源
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.slave")
    public DataSource slaveDataSource() {
        return DruidDataSourceFactory.createDataSource(
            new DruidConfig());
    }

    // 主事务管理器
    @Bean
    public PlatformTransactionManager primaryTransactionManager(
            @Qualifier("primaryDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    // 从事务管理器
    @Bean
    public PlatformTransactionManager slaveTransactionManager(
            @Qualifier("slaveDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

2. 配置示例

spring:
  datasource:
    primary:
      url: jdbc:mysql://localhost:3306/master_db?useUnicode=true
      username: root
      password: root
      driver-class-name: com.mysql.cj.jdbc.Driver
      druid:
        initial-size: 5
        min-idle: 5
        max-active: 20
    slave:
      url: jdbc:mysql://localhost:3306/slave_db?useUnicode=true
      username: root
      password: root
      driver-class-name: com.mysql.cj.jdbc.Driver
      druid:
        initial-size: 5
        min-idle: 5
        max-active: 20

3. JPA 多数据源配置

@Configuration
@EnableJpaRepositories(
    basePackages = "com.example.demo.repository.master",
    entityManagerFactoryRef = "masterEntityManagerFactory",
    transactionManagerRef = "masterTransactionManager"
)
public class MasterJpaConfig {

    @Autowired
    @Qualifier("primaryDataSource")
    private DataSource primaryDataSource;

    @Bean
    @Primary
    public LocalContainerEntityManagerFactoryBean
        masterEntityManagerFactory(EntityManagerFactoryBuilder builder) {
        return builder
            .dataSource(primaryDataSource)
            .packages("com.example.demo.entity.master")
            .persistenceUnit("master")
            .build();
    }

    @Bean
    @Primary
    public JpaTransactionManager masterTransactionManager(
            EntityManagerFactory masterEntityManagerFactory) {
        return new JpaTransactionManager(masterEntityManagerFactory);
    }
}

@Configuration
@EnableJpaRepositories(
    basePackages = "com.example.demo.repository.slave",
    entityManagerFactoryRef = "slaveEntityManagerFactory",
    transactionManagerRef = "slaveTransactionManager"
)
public class SlaveJpaConfig {

    @Autowired
    @Qualifier("slaveDataSource")
    private DataSource slaveDataSource;

    @Bean
    public LocalContainerEntityManagerFactoryBean
        slaveEntityManagerFactory(EntityManagerFactoryBuilder builder) {
        return builder
            .dataSource(slaveDataSource)
            .packages("com.example.demo.entity.slave")
            .persistenceUnit("slave")
            .build();
    }

    @Bean
    public JpaTransactionManager slaveTransactionManager(
            EntityManagerFactory slaveEntityManagerFactory) {
        return new JpaTransactionManager(slaveEntityManagerFactory);
    }
}

4. MyBatis 多数据源配置

@Configuration
public class MyBatisConfig {

    // 主库 SqlSessionFactory
    @Bean
    @Primary
    public SqlSessionFactory masterSqlSessionFactory(
            @Qualifier("primaryDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
        factory.setDataSource(dataSource);
        factory.setMapperLocations(
            new PathMatchingResourcePatternResolver()
                .getResources("classpath:mapper/master/**/*.xml"));
        return factory.getObject();
    }

    // 从库 SqlSessionFactory
    @Bean
    public SqlSessionFactory slaveSqlSessionFactory(
            @Qualifier("slaveDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
        factory.setDataSource(dataSource);
        factory.setMapperLocations(
            new PathMatchingResourcePatternResolver()
                .getResources("classpath:mapper/slave/**/*.xml"));
        return factory.getObject();
    }
}

5. 动态数据源切换

// 动态数据源
public class DynamicDataSource extends AbstractRoutingDataSource {

    private static final ThreadLocal<String> DATA_SOURCE_KEY =
        ThreadLocal.withInitial(() -> "master");

    public static void setDataSource(String dataSource) {
        DATA_SOURCE_KEY.set(dataSource);
    }

    public static String getDataSource() {
        return DATA_SOURCE_KEY.get();
    }

    public static void clear() {
        DATA_SOURCE_KEY.remove();
    }

    @Override
    protected Object determineCurrentLookupKey() {
        return DATA_SOURCE_KEY.get();
    }
}

// 数据源切换切面
@Aspect
@Component
public class DataSourceSwitchAspect {

    @Pointcut("execution(* com.example.demo.mapper.master.*.*(..))")
    public void masterPointcut() {}

    @Pointcut("execution(* com.example.demo.mapper.slave.*.*(..))")
    public void slavePointcut() {}

    @Before("masterPointcut()")
    public void switchToMaster() {
        DynamicDataSource.setDataSource("master");
    }

    @Before("slavePointcut()")
    public void switchToSlave() {
        DynamicDataSource.setDataSource("slave");
    }
}

6. 读写分离路由

@Service
public class RoutingDataSourceService {

    @Autowired
    private DynamicDataSource dynamicDataSource;

    // 读操作切换到从库
    public User findById(Long id) {
        DynamicDataSource.setDataSource("slave");
        try {
            return userMapper.findById(id);
        } finally {
            DynamicDataSource.clear();
        }
    }

    // 写操作使用主库
    @Transactional(transactionManager = "masterTransactionManager")
    public void save(User user) {
        // 默认主库
        userMapper.insert(user);
    }

    // 显式指定主库
    @Transactional(transactionManager = "masterTransactionManager")
    public void update(User user) {
        DynamicDataSource.setDataSource("master");
        try {
            userMapper.update(user);
        } finally {
            DynamicDataSource.clear();
        }
    }
}

常见坑点

⚠️ 坑 1:@Primary 未指定

// ❌ 缺少 @Primary
@Bean
public DataSource slaveDataSource() {
    return DruidDataSourceFactory.createDataSource(config);
}

// ✅ 添加 @Primary 标记默认数据源
@Bean
@Primary
public DataSource primaryDataSource() {
    return DruidDataSourceFactory.createDataSource(config);
}

⚠️ 坑 2:事务管理器未匹配

// ❌ 事务管理器与数据源不匹配
@Transactional(transactionManager = "slaveTransactionManager")
public void save(User user) {
    // 实际使用 masterDataSource,但事务管理器是 slave
}

// ✅ 确保匹配
@Transactional(transactionManager = "primaryTransactionManager")
public void save(User user) {
    // 使用 primaryDataSource
}

⚠️ 坑 3:跨数据源事务

// ❌ 跨数据源无法用本地事务
@Transactional
public void distributedOperation() {
    userMapper.insert(user);         // 主库
    orderMapper.insert(order);       // 从库 - 不会回滚!
}

// ✅ 使用分布式事务(Seata)
@GlobalTransactional
public void distributedOperation() {
    userMapper.insert(user);         // 主库
    orderMapper.insert(order);       // 从库
}

面试题

Q1:如何实现读写分离?

参考答案

  1. 配置多数据源:主库(写)+ 从库(读)
  2. 数据源路由:根据操作类型自动切换
  3. 实现方式
    • AOP 切面:根据 Mapper 所在包切换
    • 注解:自定义 @ReadOnly 注解
    • 动态路由:继承 AbstractRoutingDataSource

Q2:Spring 如何管理多数据源事务?

参考答案

每个数据源需要独立的 TransactionManager

@Bean
public PlatformTransactionManager masterTxManager(
        @Qualifier("primaryDataSource") DataSource ds) {
    return new DataSourceTransactionManager(ds);
}

@Bean
public PlatformTransactionManager slaveTxManager(
        @Qualifier("slaveDataSource") DataSource ds) {
    return new DataSourceTransactionManager(ds);
}

使用事务时指定:

@Transactional(transactionManager = "masterTxManager")
public void writeOperation() { }

Q3:多数据源下的分布式事务?

参考答案

方案代表说明
XAAtomikos两阶段提交,性能差
TCCSeataTry-Confirm-Cancel
最终一致性本地消息表不保证实时一致

Seata AT 模式

@GlobalTransactional
public void purchase(Order order) {
    // 扣库存 - branch 1
    inventoryMapper.decrease(order.getProductId(), order.getQuantity());
    // 扣余额 - branch 2
    accountMapper.decrease(order.getUserId(), order.getAmount());
}