SpringBoot + AbstractRoutingDataSource实现动态切换数据源,这样做才更优雅!

引言:多数据源的那些事儿

系统刚开始只有一个数据库,后来业务发展了,需要分库分表?或者要连接多个不同的数据库,比如主库、从库、日志库?再或者要做多租户系统,每个租户独立数据库?这时候,动态切换数据源就成了必须掌握的技能。今天我们就来聊聊如何在SpringBoot中优雅地实现动态数据源切换,让多数据源管理变得简单高效。

为什么需要动态数据源?

先说说为什么我们需要动态切换数据源。

想象一下,你是一家SaaS公司的后端工程师。公司有1000个租户,如果所有租户都用同一个数据库,随着租户增多,数据库压力会越来越大,查询变慢,维护困难。但如果每个租户独立数据库,又需要动态切换数据源。

动态数据源的优势:

  1. 性能提升:分库分表减轻单库压力
  2. 数据隔离:不同业务数据物理隔离
  3. 扩展性好:便于横向扩展
  4. 维护方便:独立管理不同业务数据

传统做法的问题

在介绍优雅方案之前,先看看传统做法有什么问题:

问题一:硬编码方式

// 硬编码,不灵活
public User getUserFromMaster(Long id) {
    // 直接指定连接主库
    return jdbcTemplateMaster.queryForObject(sql, User.class, id);
}

public User getUserFromSlave(Long id) {
    // 直接指定连接从库
    return jdbcTemplateSlave.queryForObject(sql, User.class, id);
}

这种方式的问题:

  • 代码重复多
  • 不灵活,无法动态调整
  • 维护困难

问题二:配置繁琐

传统的多数据源配置往往需要为每个数据源都配置一套完整的Bean,代码冗余严重。

优雅实现方案

核心思路

Spring的AbstractRoutingDataSource为我们提供了解决方案。它是一个数据源路由处理器,可以根据不同的key动态选择目标数据源。

实现步骤

1. 自定义路由数据源

public class DynamicDataSource extends AbstractRoutingDataSource {
    
    @Override
    protected Object determineCurrentLookupKey() {
        // 从ThreadLocal中获取当前数据源key
        return DynamicDataSourceContextHolder.getDataSourceKey();
    }
}

2. 数据源上下文管理

public class DynamicDataSourceContextHolder {
    
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
    
    public static void setDataSourceKey(String key) {
        CONTEXT_HOLDER.set(key);
    }
    
    public static String getDataSourceKey() {
        return CONTEXT_HOLDER.get();
    }
    
    public static void clearDataSourceKey() {
        CONTEXT_HOLDER.remove();
    }
}

3. 配置多数据源

@Configuration
public class DataSourceConfig {
    
    @Bean
    @ConfigurationProperties("spring.datasource.master")
    public DataSource masterDataSource() {
        return DruidDataSourceBuilder.create().build();
    }
    
    @Bean
    @ConfigurationProperties("spring.datasource.slave")
    public DataSource slaveDataSource() {
        return DruidDataSourceBuilder.create().build();
    }
    
    @Bean
    @Primary
    public DataSource dynamicDataSource() {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        
        // 设置默认数据源
        dynamicDataSource.setDefaultTargetDataSource(masterDataSource());
        
        // 设置所有数据源
        Map<Object, Object> dataSourceMap = new HashMap<>();
        dataSourceMap.put("master", masterDataSource());
        dataSourceMap.put("slave", slaveDataSource());
        
        dynamicDataSource.setTargetDataSources(dataSourceMap);
        
        return dynamicDataSource;
    }
}

高级特性实现

1. 基于注解的数据源切换

定义一个注解来标识数据源:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
    String value() default "master";
}

创建切面来处理注解:

@Aspect
@Component
public class DataSourceAspect {
    
    @Before("@annotation(ds)")
    public void setDataSource(JoinPoint point, DataSource ds) {
        DynamicDataSourceContextHolder.setDataSourceKey(ds.value());
    }
    
    @After("@annotation(ds)")
    public void clearDataSource(JoinPoint point, DataSource ds) {
        DynamicDataSourceContextHolder.clearDataSourceKey();
    }
}

2. 读写分离实现

在服务层使用注解实现读写分离:

@Service
public class UserService {
    
    @Autowired
    private UserMapper userMapper;
    
    @DataSource("master") // 写操作使用主库
    public void saveUser(User user) {
        userMapper.insert(user);
    }
    
    @DataSource("slave") // 读操作使用从库
    public User getUserById(Long id) {
        return userMapper.selectById(id);
    }
}

3. 多租户数据源切换

对于多租户场景,可以根据租户ID动态切换数据源:

@Component
public class TenantDataSourceRouter {
    
    public void setTenantDataSource(String tenantId) {
        // 根据租户ID构建数据源key
        String dataSourceKey = "tenant_" + tenantId;
        DynamicDataSourceContextHolder.setDataSourceKey(dataSourceKey);
    }
}

实现细节与最佳实践

1. 事务处理

在使用动态数据源时,事务处理需要特别注意:

@Service
public class OrderService {
    
    @Transactional
    @DataSource("master")
    public void processOrder(Order order) {
        // 所有操作都在同一个数据源和事务中
        orderMapper.insert(order);
        orderItemMapper.insertBatch(order.getItems());
    }
}

2. 连接池管理

合理配置连接池参数,避免连接泄露:

spring:
  datasource:
    master:
      type: com.alibaba.druid.pool.DruidDataSource
      druid:
        initial-size: 5
        min-idle: 5
        max-active: 20
        max-wait: 60000
        time-between-eviction-runs-millis: 60000
        min-evictable-idle-time-millis: 300000

3. 异步处理中的数据源切换

在异步方法中需要特别处理数据源切换:

@Async
@DataSource("slave")
public CompletableFuture<List<User>> getUsersAsync() {
    // 异步方法中数据源切换
    List<User> users = userMapper.selectAll();
    return CompletableFuture.completedFuture(users);
}

性能优化建议

  1. 缓存数据源key:避免频繁的ThreadLocal操作
  2. 合理配置连接池:根据业务特点调整连接池大小
  3. 监控数据源使用情况:及时发现数据源切换问题
  4. 连接复用:在同一线程中复用数据源连接

常见问题与解决方案

问题1:数据源切换失效

原因:AOP切面优先级问题
解决方案:调整切面优先级,确保数据源切换在事务之前

问题2:事务跨数据源

原因:分布式事务处理不当
解决方案:避免跨数据源事务,或使用分布式事务框架

问题3:内存泄漏

原因:ThreadLocal未清理
解决方案:确保在方法结束后清理ThreadLocal

总结

通过SpringBoot + AbstractRoutingDataSource,我们可以优雅地实现动态数据源切换。关键在于:

  1. 合理架构:路由数据源、上下文管理、AOP切面
  2. 注解驱动:使用注解简化数据源切换
  3. 性能优化:合理配置连接池、监控使用情况
  4. 问题处理:注意事务、内存泄漏等常见问题

记住,动态数据源虽然强大,但也要根据业务场景合理使用。掌握了这些技巧,你就能轻松应对各种多数据源场景,让系统更加灵活和高效。


标题:SpringBoot + AbstractRoutingDataSource实现动态切换数据源,这样做才更优雅!
作者:jiangyi
地址:http://www.jiangyi.space/articles/2025/12/30/1767073873374.html

    0 评论
avatar