Java 配置多数据源

39 阅读2分钟

1. 项目中使用多数据源的配置, 模拟一个场景, 配置手动的读写分离, 当方法前缀为 query get select find count 的方法为读请求, 就使用从数据库, 否则就使用主库. 但是如果我没有配置从库, 那么就使用默认的数据源 (主库).

spring:
 datasource:
    type: com.zaxxer.hikari.HikariDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    jdbc-url: jdbc:mysql://127.0.0.1:3307/v3_admin_vite?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=true
    url: jdbc:mysql://60.204.158.66:3306/v3_admin_vite?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=true
    username: root
    password: 123456
    slave:   
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://127.0.0.1:3306/v3_admin_vite?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=true
      username: root
      password: 123456

2. 多数据源配置类 DataSourceConfig,主要是创建动态数据源, 事务管理器 和 SQL会话工厂, 使用了条件注解, 只有在配置了从库之后才被加载, 保证了未设置从库的情况会使用默认配置, 默认数据源.

@Configuration
@ConditionalOnProperty(prefix = "spring.datasource.slave", name = "jdbc-url")
@MapperScan(basePackages = "com.v3admin.mapper", sqlSessionFactoryRef = "sqlSessionFactory")
public class DataSourceConfig {

    // 主库配置
    @Bean(name = "masterDataSource")
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }

    // 从库配置(可选)
    @Bean(name = "slaveDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.slave")
    public DataSource slaveDataSource() {
        try {
            return DataSourceBuilder.create().build();
        } catch (Exception e) {
            // 如果从库配置不存在或无效,返回null
            return null;
        }
    }

    // 动态数据源配置
    @Bean(name = "dynamicDataSource")
    public DataSource dynamicDataSource(
            @Qualifier("masterDataSource") DataSource masterDataSource,
            @Qualifier("slaveDataSource") DataSource slaveDataSource) {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("master", masterDataSource);
        // 只有当从库数据源有效时才添加
        if (((HikariDataSource)slaveDataSource).getJdbcUrl() != null) {
            targetDataSources.put("slave", slaveDataSource);
        }

        dynamicDataSource.setTargetDataSources(targetDataSources);
        dynamicDataSource.setDefaultTargetDataSource(masterDataSource); // 默认使用主库
        return dynamicDataSource;
    }

    // 配置 SqlSessionFactory
    @Bean(name = "sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dataSource) throws Exception {
        MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:/mapper/**/*.xml"));
        return bean.getObject();
    }

    // 配置事务管理器
    @Bean(name = "transactionManager")
    public DataSourceTransactionManager transactionManager(@Qualifier("dynamicDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

3. 使用了条件注解, 只有配置类被加载了,才去加载动态数据源类

@Component
@ConditionalOnBean(DataSourceConfig.class)
public class DynamicDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDataSource(); // 从 ThreadLocal 获取当前数据源标识
    }

    /**
     * 获取已解析的数据源映射
     * @return 数据源映射
     */
    public Map<Object, DataSource> getResolvedDataSources() {
        return super.getResolvedDataSources();
    }
}

将当前使用的数据源的 key 放入线程本地变量里面, 到时候动态数据源会自动获取

public class DynamicDataSourceContextHolder {
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

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

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

    public static void clearDataSource() {
        CONTEXT_HOLDER.remove();
    }
}

4. AOP 类, 获取 get, select, find, query, count 开头的方法, 对它使用从库, 其他的默认是写操作, 使用主库!

也加了条件注解, @ConditionalOnBean(DataSourceConfig.class), 当配置类未加载, 这个 AOP 也不加载.

@Slf4j
@Aspect
@Component
@ConditionalOnBean(DataSourceConfig.class)
public class DynamicDataSourceAspect {

    // 定义需要走从库的方法名前缀
    private static final Set<String> SLAVE_PREFIXES = new HashSet<>(
            Arrays.asList("get", "select", "find", "query", "count"));

    private DynamicDataSource dynamicDataSource;

    @Autowired
    public void setDynamicDataSource(DynamicDataSource dynamicDataSource) {
        this.dynamicDataSource = dynamicDataSource;
    }

    // 拦截 Service 层方法
    @Pointcut("execution(* com.v3admin.service..*.*(..))")
    public void serviceMethods() {}

    // 环绕通知:根据方法名自动切换数据源
    @Around("serviceMethods()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        String methodName = joinPoint.getSignature().getName();

        // 判断方法名是否匹配从库前缀
        boolean isSlaveMethod = SLAVE_PREFIXES.stream()
                .anyMatch(methodName::startsWith);

        try {
            if (isSlaveMethod && hasSlaveDataSource()) {
                DynamicDataSourceContextHolder.setDataSource("slave");
                log.info("使用从库");
            } else {
                DynamicDataSourceContextHolder.setDataSource("master");
                if (isSlaveMethod && !hasSlaveDataSource()) {
                    log.info("查询操作本应使用从库,但未配置从库,使用主库");
                } else {
                    log.info("使用主库");
                }
            }
            return joinPoint.proceed(); // 执行目标方法
        } finally {
            DynamicDataSourceContextHolder.clearDataSource(); // 清除数据源
        }
    }

    /**
     * 检查是否存在从库数据源
     * @return 是否存在从库
     */
    private boolean hasSlaveDataSource() {
        Map<Object, DataSource> targetDataSources = dynamicDataSource.getResolvedDataSources();
        return targetDataSources != null && targetDataSources.containsKey("slave");
    }
}