聊聊动态数据源

前言

咱们星球中的商城系统中使用了动态数据源的功能,实现了分库分表的订单库的读库和写库的自动切换。

有球友反馈说,对动态数据源不太熟悉。

今天这篇文章就专门跟大家一起聊聊动态数据源,希望对你会有所帮助。

一、为什么需要动态数据源?

有些小伙伴在开发中可能会遇到这样的场景:一个系统需要同时访问多个数据库,或者需要根据业务参数动态选择数据源。这

时候,传统的单数据源配置就显得力不从心了。

1.1 传统多数据源的问题

传统方式的多个数据源配置,硬编码,不灵活。

例如下面这样:

java 复制代码
@Configuration
public class TraditionalDataSourceConfig {
    
    @Bean
    @Primary
    public DataSource primaryDataSource() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/db1");
        dataSource.setUsername("user1");
        dataSource.setPassword("pass1");
        return dataSource;
    }
    
    @Bean
    public DataSource secondaryDataSource() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/db2");
        dataSource.setUsername("user2");
        dataSource.setPassword("pass2");
        return dataSource;
    }
}

使用时需要手动管理数据源。

java 复制代码
@Repository
public class TraditionalUserDao {
    
    @Autowired
    @Qualifier("primaryDataSource")
    private DataSource primaryDataSource;
    
    @Autowired
    @Qualifier("secondaryDataSource")
    private DataSource secondaryDataSource;
    
    public User findUserFromPrimary(Long id) {
        // 需要手动获取连接、处理异常、关闭连接
        try (Connection conn = primaryDataSource.getConnection();
             PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE id = ?")) {
            stmt.setLong(1, id);
            ResultSet rs = stmt.executeQuery();
            // 处理结果集...
        } catch (SQLException e) {
            throw new RuntimeException("查询失败", e);
        }
    }
  
}

每个方法都要重复这样的模板代码,需要手动指定数据源,很麻烦。

那么,如何做优化呢?

1.2 动态数据源的优势

接下来,我们一起看看使用动态数据源后的优雅代码。

java 复制代码
@Service
public class UserService {
    
    @Autowired
    private UserMapper userMapper;
    
    // 根据租户ID自动选择数据源
    public User findUserByTenant(Long userId, String tenantId) {
        // 设置数据源上下文
        DataSourceContextHolder.setDataSource(tenantId);
        try {
            return userMapper.selectById(userId);
        } finally {
            // 清理上下文
            DataSourceContextHolder.clear();
        }
    }
    
    // 多租户数据聚合查询
    public UserAggregateInfo getUserAggregateInfo(Long userId) {
        UserAggregateInfo result = new UserAggregateInfo();
        
        // 查询主库
        DataSourceContextHolder.setDataSource("master");
        result.setBaseInfo(userMapper.selectById(userId));
        
        // 查询归档库
        DataSourceContextHolder.setDataSource("archive");
        result.setHistory(userMapper.selectHistory(userId));
        
        // 查询统计库
        DataSourceContextHolder.setDataSource("stats");
        result.setStatistics(userMapper.selectStats(userId));
        
        return result;
    }
}

代码中能根据租户ID自动选择数据源。

代码一下子变得更优雅了。

二、动态数据源的原理

有些小伙伴在使用动态数据源时,可能只是简单配置使用,并不清楚其底层工作原理。

理解核心原理对于排查问题和性能优化至关重要。

下面跟大家一起聊聊动态数据源的核心原理,希望对你会有所帮助。

数据源路由的核心机制

动态数据源的核心在于AbstractRoutingDataSource,它是Spring框架提供的抽象类:

java 复制代码
// Spring AbstractRoutingDataSource 源码分析
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
    
    // 目标数据源映射表
    private Map<Object, Object> targetDataSources;
    
    // 默认数据源
    private Object defaultTargetDataSource;
    
    // 解析后的数据源映射
    private Map<Object, DataSource> resolvedDataSources;
    
    // 解析后的默认数据源
    private DataSource resolvedDefaultDataSource;
    
    // 关键方法:确定当前查找键
    protected abstract Object determineCurrentLookupKey();
    
    // 获取连接时选择数据源
    @Override
    public Connection getConnection() throws SQLException {
        return determineTargetDataSource().getConnection();
    }
    
    @Override
    public Connection getConnection(String username, String password) throws SQLException {
        return determineTargetDataSource().getConnection(username, password);
    }
    
    // 确定目标数据源
    protected DataSource determineTargetDataSource() {
        // 获取查找键
        Object lookupKey = determineCurrentLookupKey();
        
        // 根据查找键获取数据源
        DataSource dataSource = this.resolvedDataSources.get(lookupKey);
        if (dataSource == null && (this.resolvedDefaultDataSource != null || lookupKey == null)) {
            dataSource = this.resolvedDefaultDataSource;
        }
        if (dataSource == null) {
            throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
        }
        return dataSource;
    }
}

线程安全的数据源上下文管理

java 复制代码
/**
 * 数据源上下文管理器 - 核心组件
 * 使用ThreadLocal保证线程安全
 */
public class DataSourceContextHolder {
    
    // 使用ThreadLocal保证线程隔离
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
    
    // 支持嵌套数据源切换的栈
    private static final ThreadLocal<Deque<String>> DATASOURCE_STACK = ThreadLocal.withInitial(ArrayDeque::new);
    
    // 设置数据源
    public static void setDataSource(String dataSource) {
        if (dataSource == null) {
            throw new IllegalArgumentException("数据源不能为null");
        }
        CONTEXT_HOLDER.set(dataSource);
        
        // 同时压入栈,支持嵌套调用
        DATASOURCE_STACK.get().push(dataSource);
    }
    
    // 获取当前数据源
    public static String getDataSource() {
        return CONTEXT_HOLDER.get();
    }
    
    // 清除数据源
    public static void clear() {
        CONTEXT_HOLDER.remove();
        Deque<String> stack = DATASOURCE_STACK.get();
        if (!stack.isEmpty()) {
            stack.pop();
            // 如果栈中还有元素,恢复到上一个数据源
            if (!stack.isEmpty()) {
                CONTEXT_HOLDER.set(stack.peek());
            }
        }
    }
    
    // 强制清除所有上下文(用于线程池场景)
    public static void clearCompletely() {
        CONTEXT_HOLDER.remove();
        DATASOURCE_STACK.get().clear();
    }
    
    // 判断是否已设置数据源
    public static boolean hasDataSource() {
        return CONTEXT_HOLDER.get() != null;
    }
}

/**
 * 自定义路由数据源
 */
@Component
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
    
    // 所有可用的数据源
    private final Map<Object, Object> targetDataSources = new ConcurrentHashMap<>();
    
    @Override
    protected Object determineCurrentLookupKey() {
        String dataSourceKey = DataSourceContextHolder.getDataSource();
        
        if (dataSourceKey == null) {
            // 返回默认数据源
            return "default";
        }
        
        // 验证数据源是否存在
        if (!targetDataSources.containsKey(dataSourceKey)) {
            throw new IllegalArgumentException("数据源 " + dataSourceKey + " 不存在");
        }
        
        logger.debug("当前使用数据源: {}", dataSourceKey);
        return dataSourceKey;
    }
    
    // 添加数据源
    public void addDataSource(String key, DataSource dataSource) {
        this.targetDataSources.put(key, dataSource);
        // 更新目标数据源映射
        setTargetDataSources(new HashMap<>(this.targetDataSources));
        // 重新初始化
        afterPropertiesSet();
    }
    
    // 移除数据源
    public void removeDataSource(String key) {
        if (this.targetDataSources.containsKey(key)) {
            DataSource dataSource = (DataSource) this.targetDataSources.remove(key);
            // 关闭数据源连接池
            closeDataSource(dataSource);
            // 更新目标数据源映射
            setTargetDataSources(new HashMap<>(this.targetDataSources));
            afterPropertiesSet();
        }
    }
    
    // 获取所有数据源
    public Map<Object, Object> getTargetDataSources() {
        return Collections.unmodifiableMap(targetDataSources);
    }
    
    private void closeDataSource(DataSource dataSource) {
        if (dataSource instanceof HikariDataSource) {
            ((HikariDataSource) dataSource).close();
        } else if (dataSource instanceof org.apache.tomcat.jdbc.pool.DataSource) {
            ((org.apache.tomcat.jdbc.pool.DataSource) dataSource).close();
        }
        // 其他类型的数据源关闭逻辑...
    }
}

动态数据源执行流程

三、基于Spring Boot的完整实现

有些小伙伴在配置动态数据源时可能会遇到各种问题,下面我提供一个生产级别的完整实现。

完整配置实现

java 复制代码
/**
 * 动态数据源配置类
 */
@Configuration
@EnableTransactionManagement
@EnableConfigurationProperties(DynamicDataSourceProperties.class)
public class DynamicDataSourceConfig {
    
    @Autowired
    private DynamicDataSourceProperties properties;
    
    /**
     * 主数据源(默认数据源)
     */
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }
    
    /**
     * 从数据源1
     */
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.slave1")
    public DataSource slave1DataSource() {
        return DataSourceBuilder.create().build();
    }
    
    /**
     * 从数据源2
     */
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.slave2")
    public DataSource slave2DataSource() {
        return DataSourceBuilder.create().build();
    }
    
    /**
     * 动态数据源
     */
    @Bean
    @Primary
    public DataSource dynamicDataSource(DataSource masterDataSource, 
                                       DataSource slave1DataSource, 
                                       DataSource slave2DataSource) {
        
        Map<Object, Object> targetDataSources = new HashMap<>(8);
        targetDataSources.put("master", masterDataSource);
        targetDataSources.put("slave1", slave1DataSource);
        targetDataSources.put("slave2", slave2DataSource);
        
        DynamicRoutingDataSource dynamicDataSource = new DynamicRoutingDataSource();
        // 设置默认数据源
        dynamicDataSource.setDefaultTargetDataSource(masterDataSource);
        // 设置目标数据源
        dynamicDataSource.setTargetDataSources(targetDataSources);
        
        return dynamicDataSource;
    }
    
    /**
     * 事务管理器
     */
    @Bean
    public PlatformTransactionManager transactionManager(DataSource dynamicDataSource) {
        return new DataSourceTransactionManager(dynamicDataSource);
    }
    
    /**
     * MyBatis配置
     */
    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dynamicDataSource) throws Exception {
        SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dynamicDataSource);
        
        // 配置MyBatis
        org.apache.ibatis.session.Configuration configuration = 
            new org.apache.ibatis.session.Configuration();
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.setCacheEnabled(true);
        configuration.setLazyLoadingEnabled(false);
        configuration.setAggressiveLazyLoading(false);
        sessionFactory.setConfiguration(configuration);
        
        return sessionFactory.getObject();
    }
}

/**
 * 数据源配置属性类
 */
@ConfigurationProperties(prefix = "spring.datasource")
@Data
public class DynamicDataSourceProperties {
    
    /**
     * 主数据源配置
     */
    private HikariConfig master = new HikariConfig();
    
    /**
     * 从数据源1配置
     */
    private HikariConfig slave1 = new HikariConfig();
    
    /**
     * 从数据源2配置
     */
    private HikariConfig slave2 = new HikariConfig();
    
    /**
     * 动态数据源配置
     */
    private DynamicConfig dynamic = new DynamicConfig();
    
    @Data
    public static class DynamicConfig {
        /**
         * 默认数据源
         */
        private String primary = "master";
        
        /**
         * 是否开启严格模式
         */
        private boolean strict = false;
        
        /**
         * 数据源健康检查间隔(秒)
         */
        private long healthCheckInterval = 30;
    }
}

应用配置文件

yaml 复制代码
# application.yml
spring:
  datasource:
    # 动态数据源配置
    dynamic:
      primary: master
      strict: true
      health-check-interval: 30
    
    # 主数据源
    master:
      jdbc-url: jdbc:mysql://localhost:3306/master_db?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true
      username: root
      password: master_password
      driver-class-name: com.mysql.cj.jdbc.Driver
      maximum-pool-size: 20
      minimum-idle: 5
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000
      pool-name: MasterHikariPool
    
    # 从数据源1
    slave1:
      jdbc-url: jdbc:mysql://slave1:3306/slave_db?useUnicode=true&characterEncoding=utf8
      username: root
      password: slave1_password
      driver-class-name: com.mysql.cj.jdbc.Driver
      maximum-pool-size: 15
      minimum-idle: 3
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000
      pool-name: Slave1HikariPool
    
    # 从数据源2
    slave2:
      jdbc-url: jdbc:mysql://slave2:3306/slave_db?useUnicode=true&characterEncoding=utf8
      username: root
      password: slave2_password
      driver-class-name: com.mysql.cj.jdbc.Driver
      maximum-pool-size: 15
      minimum-idle: 3
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000
      pool-name: Slave2HikariPool

# MyBatis配置
mybatis:
  configuration:
    map-underscore-to-camel-case: true
    cache-enabled: true
    lazy-loading-enabled: false
    aggressive-lazy-loading: false

注解式数据源切换

java 复制代码
/**
 * 数据源注解
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
    
    /**
     * 数据源名称
     */
    String value() default "master";
    
    /**
     * 是否在方法执行后清除数据源(默认清除)
     */
    boolean clear() default true;
}

/**
 * 数据源切面
 */
@Aspect
@Component
@Slf4j
public class DataSourceAspect {
    
    /**
     * 定义切点:所有标注@DataSource注解的方法
     */
    @Pointcut("@annotation(com.example.annotation.DataSource)")
    public void dataSourcePointCut() {}
    
    /**
     * 环绕通知:在方法执行前后切换数据源
     */
    @Around("dataSourcePointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        
        DataSource dataSourceAnnotation = method.getAnnotation(DataSource.class);
        if (dataSourceAnnotation == null) {
            // 类级别注解
            dataSourceAnnotation = point.getTarget().getClass().getAnnotation(DataSource.class);
        }
        
        if (dataSourceAnnotation != null) {
            String dataSourceKey = dataSourceAnnotation.value();
            boolean clearAfter = dataSourceAnnotation.clear();
            
            try {
                log.debug("切换数据源到: {}", dataSourceKey);
                DataSourceContextHolder.setDataSource(dataSourceKey);
                
                // 执行原方法
                return point.proceed();
                
            } finally {
                if (clearAfter) {
                    DataSourceContextHolder.clear();
                    log.debug("清除数据源上下文");
                }
            }
        }
        
        // 没有注解,使用默认数据源
        return point.proceed();
    }
}

四、高级特性

有些小伙伴在基础功能实现后,可能会遇到一些高级场景的需求。

下面介绍几个生产环境中常用的高级特性。

读写分离自动路由

java 复制代码
/**
 * 读写分离数据源路由器
 */
@Component
@Slf4j
public class ReadWriteDataSourceRouter {
    
    // 读数据源列表
    private final List<String> readDataSources = Arrays.asList("slave1", "slave2");
    
    // 轮询计数器
    private final AtomicInteger counter = new AtomicInteger(0);
    
    /**
     * 根据SQL自动选择数据源
     */
    public String determineDataSource(boolean isReadOperation) {
        if (isReadOperation && !readDataSources.isEmpty()) {
            // 读操作:轮询选择从库
            int index = counter.getAndIncrement() % readDataSources.size();
            if (counter.get() > 9999) {
                counter.set(0); // 防止溢出
            }
            String readDataSource = readDataSources.get(index);
            log.debug("读操作选择数据源: {}", readDataSource);
            return readDataSource;
        } else {
            // 写操作:选择主库
            log.debug("写操作选择数据源: master");
            return "master";
        }
    }
    
    /**
     * 根据SQL语句判断是否为读操作
     */
    public boolean isReadOperation(String sql) {
        if (sql == null) {
            return true; // 默认为读操作
        }
        
        String trimmedSql = sql.trim().toLowerCase();
        return trimmedSql.startsWith("select") || 
               trimmedSql.startsWith("show") ||
               trimmedSql.startsWith("explain");
    }
}

/**
 * MyBatis拦截器 - 自动读写分离
 */
@Intercepts({
    @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
    @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
@Component
@Slf4j
public class ReadWriteInterceptor implements Interceptor {
    
    @Autowired
    private ReadWriteDataSourceRouter dataSourceRouter;
    
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        String methodName = invocation.getMethod().getName();
        MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
        
        boolean isReadOperation = "query".equals(methodName);
        String sql = getSql(ms, invocation.getArgs()[1]);
        
        // 如果当前没有手动设置数据源,则自动选择
        if (!DataSourceContextHolder.hasDataSource()) {
            String dataSource = dataSourceRouter.determineDataSource(isReadOperation);
            DataSourceContextHolder.setDataSource(dataSource);
            
            try {
                return invocation.proceed();
            } finally {
                DataSourceContextHolder.clear();
            }
        }
        
        return invocation.proceed();
    }
    
    private String getSql(MappedStatement ms, Object parameter) {
        BoundSql boundSql = ms.getBoundSql(parameter);
        return boundSql.getSql();
    }
}

多租户数据源管理

java 复制代码
/**
 * 多租户数据源管理器
 */
@Component
@Slf4j
public class TenantDataSourceManager {
    
    @Autowired
    private DynamicRoutingDataSource dynamicRoutingDataSource;
    
    @Autowired
    private DataSourceProperties dataSourceProperties;
    
    // 租户数据源配置缓存
    private final Map<String, TenantDataSourceConfig> tenantConfigCache = new ConcurrentHashMap<>();
    
    /**
     * 根据租户ID获取数据源
     */
    public DataSource getDataSourceForTenant(String tenantId) {
        String dataSourceKey = "tenant_" + tenantId;
        
        // 检查是否已存在数据源
        if (dynamicRoutingDataSource.getTargetDataSources().containsKey(dataSourceKey)) {
            return (DataSource) dynamicRoutingDataSource.getTargetDataSources().get(dataSourceKey);
        }
        
        // 动态创建数据源
        synchronized (this) {
            if (!dynamicRoutingDataSource.getTargetDataSources().containsKey(dataSourceKey)) {
                DataSource dataSource = createTenantDataSource(tenantId);
                dynamicRoutingDataSource.addDataSource(dataSourceKey, dataSource);
                log.info("为租户 {} 创建数据源: {}", tenantId, dataSourceKey);
            }
        }
        
        return (DataSource) dynamicRoutingDataSource.getTargetDataSources().get(dataSourceKey);
    }
    
    /**
     * 动态创建租户数据源
     */
    private DataSource createTenantDataSource(String tenantId) {
        TenantDataSourceConfig config = getTenantConfig(tenantId);
        
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(buildJdbcUrl(config));
        dataSource.setUsername(config.getUsername());
        dataSource.setPassword(config.getPassword());
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setMaximumPoolSize(10);
        dataSource.setMinimumIdle(2);
        dataSource.setConnectionTimeout(30000);
        dataSource.setIdleTimeout(600000);
        dataSource.setMaxLifetime(1800000);
        dataSource.setPoolName("TenantPool_" + tenantId);
        
        return dataSource;
    }
    
    /**
     * 获取租户数据源配置(可从配置中心或数据库获取)
     */
    private TenantDataSourceConfig getTenantConfig(String tenantId) {
        return tenantConfigCache.computeIfAbsent(tenantId, key -> {
            // 这里可以从配置中心、数据库或缓存中获取租户配置
            // 简化实现,实际项目中需要完善
            TenantDataSourceConfig config = new TenantDataSourceConfig();
            config.setHost("tenant-" + tenantId + ".db.example.com");
            config.setPort(3306);
            config.setDatabase("tenant_" + tenantId);
            config.setUsername("tenant_" + tenantId);
            config.setPassword("password_" + tenantId);
            return config;
        });
    }
    
    private String buildJdbcUrl(TenantDataSourceConfig config) {
        return String.format("jdbc:mysql://%s:%d/%s?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true",
                config.getHost(), config.getPort(), config.getDatabase());
    }
    
    @Data
    public static class TenantDataSourceConfig {
        private String host;
        private int port;
        private String database;
        private String username;
        private String password;
    }
}

数据源健康监控

java 复制代码
/**
 * 数据源健康监控器
 */
@Component
@Slf4j
public class DataSourceHealthMonitor {
    
    @Autowired
    private DynamicRoutingDataSource dynamicRoutingDataSource;
    
    private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
    
    // 健康状态缓存
    private final Map<String, Boolean> healthStatus = new ConcurrentHashMap<>();
    
    @PostConstruct
    public void init() {
        // 启动健康检查任务
        scheduler.scheduleAtFixedRate(this::checkAllDataSources, 0, 30, TimeUnit.SECONDS);
    }
    
    /**
     * 检查所有数据源的健康状态
     */
    public void checkAllDataSources() {
        Map<Object, Object> dataSources = dynamicRoutingDataSource.getTargetDataSources();
        
        for (Map.Entry<Object, Object> entry : dataSources.entrySet()) {
            String dataSourceKey = (String) entry.getKey();
            DataSource dataSource = (DataSource) entry.getValue();
            
            boolean isHealthy = checkDataSourceHealth(dataSource);
            healthStatus.put(dataSourceKey, isHealthy);
            
            if (!isHealthy) {
                log.warn("数据源 {} 健康检查失败", dataSourceKey);
                // 可以发送告警通知
            }
        }
    }
    
    /**
     * 检查单个数据源健康状态
     */
    private boolean checkDataSourceHealth(DataSource dataSource) {
        try (Connection conn = dataSource.getConnection();
             Statement stmt = conn.createStatement()) {
            
            ResultSet rs = stmt.executeQuery("SELECT 1");
            return rs.next() && rs.getInt(1) == 1;
            
        } catch (SQLException e) {
            log.error("数据源健康检查异常", e);
            return false;
        }
    }
    
    /**
     * 获取数据源健康状态
     */
    public boolean isDataSourceHealthy(String dataSourceKey) {
        return healthStatus.getOrDefault(dataSourceKey, true);
    }
    
    /**
     * 获取健康的数据源列表
     */
    public List<String> getHealthyDataSources() {
        return healthStatus.entrySet().stream()
                .filter(Map.Entry::getValue)
                .map(Map.Entry::getKey)
                .collect(Collectors.toList());
    }
    
    @PreDestroy
    public void destroy() {
        scheduler.shutdown();
    }
}

五、动态数据源的应用场景

让我们通过架构图来理解动态数据源的典型应用场景:

六、优缺点

优点

  1. 灵活性高:支持运行时动态添加、移除数据源
  2. 解耦性好:业务代码与具体数据源解耦
  3. 扩展性强:易于实现读写分离、多租户等复杂场景
  4. 维护方便:数据源配置集中管理,便于维护

缺点

  1. 复杂度增加:系统架构变得更加复杂
  2. 事务管理复杂:跨数据源事务需要特殊处理
  3. 连接池开销:每个数据源都需要独立的连接池
  4. 调试困难:数据源切换增加了调试复杂度

七、生产环境注意事项

事务管理策略

java 复制代码
/**
 * 多数据源事务管理器
 */
@Component
@Slf4j
public class MultiDataSourceTransactionManager {
    
    /**
     * 在多个数据源上执行事务性操作
     */
    @Transactional(rollbackFor = Exception.class)
    public void executeInTransaction(Runnable task, String... dataSources) {
        if (dataSources.length == 1) {
            // 单数据源事务
            DataSourceContextHolder.setDataSource(dataSources[0]);
            try {
                task.run();
            } finally {
                DataSourceContextHolder.clear();
            }
        } else {
            // 多数据源伪事务(最终一致性)
            executeWithCompensation(task, dataSources);
        }
    }
    
    /**
     * 使用补偿机制实现多数据源"事务"
     */
    private void executeWithCompensation(Runnable task, String[] dataSources) {
        List<Runnable> compensationTasks = new ArrayList<>();
        
        try {
            // 按顺序执行各个数据源的操作
            for (String dataSource : dataSources) {
                DataSourceContextHolder.setDataSource(dataSource);
                try {
                    // 执行实际业务操作
                    task.run();
                    
                    // 记录补偿操作
                    compensationTasks.add(0, createCompensationTask(dataSource));
                    
                } finally {
                    DataSourceContextHolder.clear();
                }
            }
        } catch (Exception e) {
            // 执行补偿操作
            log.error("多数据源操作失败,执行补偿操作", e);
            executeCompensation(compensationTasks);
            throw e;
        }
    }
    
    private void executeCompensation(List<Runnable> compensationTasks) {
        for (Runnable compensation : compensationTasks) {
            try {
                compensation.run();
            } catch (Exception ex) {
                log.error("补偿操作执行失败", ex);
                // 记录补偿失败,需要人工介入
            }
        }
    }
}

性能优化建议

  1. 连接池优化:根据业务特点调整各数据源连接池参数
  2. 数据源预热:应用启动时预热常用数据源
  3. 缓存策略:缓存数据源配置和路由信息
  4. 监控告警:建立完善的数据源监控体系

总结

动态数据源是一个强大的技术方案,能够很好地解决多数据源管理的复杂性。

通过本文的详细解析,我们可以看到:

  1. 核心原理 :基于AbstractRoutingDataSourceThreadLocal的上下文管理
  2. 实现方式:注解+AOP的声明式数据源切换
  3. 高级特性:读写分离、多租户、健康监控等生产级功能
  4. 适用场景:多租户、读写分离、分库分表等复杂数据架构

在实际项目中,建议根据具体业务需求选择合适的实现方案,不要过度设计。

同时,要建立完善的监控和运维体系,确保动态数据源的稳定运行。

最后说一句(求关注,别白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下我的同名公众号:苏三说技术,您的支持是我坚持写作最大的动力。

求一键三连:点赞、转发、在看。

关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的10万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。

更多项目实战在我的技术网站:http://www.susan.net.cn/project