SpringBoot实战:优雅实现动态数据源切换(附完整代码)

在实际业务开发中,你是否遇到过以下场景?
  • 读写分离:写操作发往主库,读操作发往从库,以提升系统性能。
  • 多租户系统:每个租户拥有独立的数据库,需要根据当前租户动态连接。
  • 业务分库:订单、用户等不同模块的数据存放在不同的物理数据库中。
面对这些需求,最原始的方法是为每个数据源配置一套JdbcTemplateSqlSessionFactory,然后在代码中硬编码切换。这种方式不仅繁琐,而且难以维护。今天,我们将介绍如何利用Spring框架提供的AbstractRoutingDataSource,以一种高度可扩展和优雅的方式实现动态数据源切换。

一、核心原理:路由数据源

Spring的AbstractRoutingDataSource类是实现动态数据源的基石。它是一个“数据源的路由器”,其核心方法是determineCurrentLookupKey()。该方法会在每次数据库操作前被调用,我们只需告诉它当前应该使用哪个数据源的key(例如 “master”, “slave”, “tenant_123”),它就能自动路由到对应的真实数据源。我们的实现方案主要包含三个部分:
  1. 动态数据源上下文:使用ThreadLocal来保存当前线程需要使用的数据源key,确保线程安全。
  2. 自定义路由数据源:继承AbstractRoutingDataSource,从其上下文中获取key。
  3. 注解与AOP切面:通过自定义注解和AOP,在方法执行前自动切换数据源,实现声明式编程。

二、步步为营:代码实现

步骤1:定义数据源上下文持有者

首先,我们需要一个工具类来管理当前线程的数据源标识。这里我们使用ThreadLocal,因为它能保证每个线程的变量独立,避免多线程环境下的互相干扰。
public class DynamicDataSourceContextHolder {

    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

    /**
     * 设置当前线程的数据源key
     */
    public static void setDataSourceKey(String dataSourceKey) {
        CONTEXT_HOLDER.set(dataSourceKey);
    }

    /**
     * 获取当前线程的数据源key
     */
    public static String getDataSourceKey() {
        return CONTEXT_HOLDER.get();
    }

    /**
     * 清除当前线程的数据源key
     * !!! 非常重要,防止内存泄漏 !!!
     */
    public static void clearDataSourceKey() {
        CONTEXT_HOLDER.remove();
    }
}

步骤2:创建自定义路由数据源

接下来,我们创建核心的路由数据源类,它负责根据上下文中的key进行路由。
public class DynamicDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        // 关键方法:从上下文中获取数据源key
        return DynamicDataSourceContextHolder.getDataSourceKey();
    }
}
步骤3:配置多个数据源并注入路由
在Spring配置类中,我们将主从等多个真实数据源注册为Bean,并将它们装配到我们的DynamicDataSource中。
@Configuration
public class DataSourceConfig {

    @Bean
    @ConfigurationProperties("spring.datasource.master") // 对应application.yml中的配置
    public DataSource masterDataSource() {
        return DruidDataSourceBuilder.create().build(); // 推荐使用Druid连接池
    }

    @Bean
    @ConfigurationProperties("spring.datasource.slave")
    public DataSource slaveDataSource() {
        return DruidDataSourceBuilder.create().build();
    }

    @Bean
    @Primary // 标记为默认数据源
    public DataSource dynamicDataSource() {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        
        // 配置默认数据源(当没有指定key时使用)
        dynamicDataSource.setDefaultTargetDataSource(masterDataSource());
        
        // 配置所有可选的数据源映射Map
        Map<Object, Object> dataSourceMap = new HashMap<>();
        dataSourceMap.put("master", masterDataSource());
        dataSourceMap.put("slave", slaveDataSource());
        // 未来可以继续添加新的数据源,如 dataSourceMap.put("log", logDataSource());
        
        dynamicDataSource.setTargetDataSources(dataSourceMap);
        return dynamicDataSource;
    }
}
application.yml中配置数据源连接信息:
spring:
  datasource:
    master:
      url: jdbc:mysql://localhost:3306/master_db
      username: root
      password: 123456
      driver-class-name: com.mysql.cj.jdbc.Driver
    slave:
      url: jdbc:mysql://localhost:3307/slave_db
      username: root
      password: 123456
      driver-class-name: com.mysql.cj.jdbc.Driver

步骤4:使用注解驱动切换(最优雅的方式)

为了摆脱手动调用setDataSourceKey的繁琐,我们定义一个注解。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
    String value() default "master"; // 默认为主库
}
然后,我们创建一个AOP切面,在带有@DataSource注解的方法执行前后,自动设置和清理数据源上下文。
@Aspect
@Component
@Slf4j // 可选,用于日志记录
public class DataSourceAspect {

    /**
     * 定义切点:所有带有@DataSource注解的方法
     */
    @Pointcut("@annotation(com.yourpackage.annotation.DataSource)")
    public void dataSourcePointCut() {}

    @Around("dataSourcePointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        // 获取方法上的注解
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        DataSource dsAnnotation = method.getAnnotation(DataSource.class);

        if (dsAnnotation != null) {
            String dataSourceKey = dsAnnotation.value();
            // 在方法执行前设置数据源
            DynamicDataSourceContextHolder.setDataSourceKey(dataSourceKey);
            log.debug("Set datasource to : {}", dataSourceKey);
        }

        try {
            // 执行目标方法
            return point.proceed();
        } finally {
            // 方法执行完毕后,无论如何都要清空数据源key
            DynamicDataSourceContextHolder.clearDataSourceKey();
            log.debug("Clear datasource.");
        }
    }
}

三、实践应用:如何在业务中使用

现在,一切准备就绪!在Service层中,你可以通过一个简单的注解来轻松切换数据源。

场景1:读写分离

@Service
@Slf4j
public class UserService {

    @Autowired
    private UserMapper userMapper;

    // 写操作使用主库
    @DataSource("master")
    public void createUser(User user) {
        userMapper.insert(user);
    }

    // 读操作使用从库,减轻主库压力
    @DataSource("slave")
    public User getUserById(Long id) {
        return userMapper.selectById(id);
    }

    @DataSource("slave")
    public List<User> getUserList() {
        return userMapper.selectList();
    }
}

场景2:多租户系统

假设你的租户数据源是动态加载的,你可以通过一个工具类在拦截器或过滤器中设置数据源。
@Component
public class TenantContext {

    public static void setCurrentTenant(String tenantId) {
        // 假设数据源key的规则是 "tenant_" + tenantId
        // 这些数据源可以在系统启动时或首次使用时加载到DynamicDataSource的映射中
        String dataSourceKey = "tenant_" + tenantId;
        DynamicDataSourceContextHolder.setDataSourceKey(dataSourceKey);
    }
}

四、重要注意事项(避坑指南)

  1. 事务管理@Transactional注解开启的事务,其数据源是在方法开始时确定的。务必确保@DataSource注解在事务注解之前生效。通常AOP默认顺序可能有问题,可以通过@Order注解调整切面优先级,使其高于事务切面。
  2. 资源清理:AOP切面中的finally块至关重要,它保证了即使在方法抛出异常的情况下,也能清空ThreadLocal,避免内存泄漏和后续操作的数据源污染。
  3. 异步场景:在@Async异步方法中,由于线程切换,ThreadLocal值不会自动传递。你需要在调用异步方法前,将数据源key作为参数显式传递,并在异步方法的开头重新设置。

总结

  通过AbstractRoutingDataSource+ ThreadLocal+ 注解&AOP的组合,我们构建了一个灵活、非侵入式的动态数据源切换方案。这套架构不仅代码清晰,而且扩展性极强,新增一个数据源只需在配置Map中添加即可,业务代码几乎无需改动。希望这篇实战博文能帮助你轻松搞定SpringBoot中的多数据源管理,让你的系统架构更加优雅和强大!
注意:以上代码为示例片段,在实际项目中请根据你的包结构和业务逻辑进行适当调整,并做好异常处理。
posted @ 2026-01-13 20:15  东峰叵,com  阅读(0)  评论(0)    收藏  举报