MST
星途 面试题库

面试题:Java JDBC数据源配置:多数据源动态切换

在一个Java项目中,需要根据用户的不同请求动态切换数据源(如读写分离场景,读操作切换到从库,写操作切换到主库)。请设计一个基于Spring Boot和Java JDBC的多数据源动态切换方案,详细描述数据源的配置过程、动态切换的实现逻辑以及可能遇到的问题和解决方案。
11.6万 热度难度
编程语言Java

知识考点

AI 面试

面试题答案

一键面试

数据源的配置过程

  1. 引入依赖:在pom.xml中添加Spring Boot JDBC和数据库连接相关依赖,如spring-boot-starter-jdbc,以及具体数据库的驱动依赖(如mysql-connector-java)。
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
  1. 配置数据源:在application.yml中配置主从数据源的连接信息。
spring:
  datasource:
    master:
      url: jdbc:mysql://master-url:3306/master_db
      username: master_user
      password: master_password
      driver-class-name: com.mysql.cj.jdbc.Driver
    slave:
      url: jdbc:mysql://slave-url:3306/slave_db
      username: slave_user
      password: slave_password
      driver-class-name: com.mysql.cj.jdbc.Driver
  1. 创建数据源Bean:创建DataSourceConfig类,用于创建主从数据源的DataSource Bean。
import org.apache.commons.dbcp2.BasicDataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;

import javax.sql.DataSource;

@Configuration
public class DataSourceConfig {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource masterDataSource() {
        return new BasicDataSource();
    }

    @Bean
    public JdbcTemplate masterJdbcTemplate(@Qualifier("masterDataSource") DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.slave")
    public DataSource slaveDataSource() {
        return new BasicDataSource();
    }

    @Bean
    public JdbcTemplate slaveJdbcTemplate(@Qualifier("slaveDataSource") DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
}

动态切换的实现逻辑

  1. 定义数据源注解:创建一个自定义注解@DataSource,用于标记需要使用特定数据源的方法或类。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface DataSource {
    String value() default "master";
}
  1. 创建数据源上下文:创建DataSourceContextHolder类,用于存储和获取当前线程使用的数据源。
public class DataSourceContextHolder {
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();

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

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

    public static void clearDataSource() {
        contextHolder.remove();
    }
}
  1. 实现数据源路由:创建DynamicDataSource类,继承AbstractRoutingDataSource,重写determineCurrentLookupKey方法来确定当前使用的数据源。
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSource();
    }
}
  1. 切面实现动态切换:创建切面类DataSourceAspect,在方法执行前根据@DataSource注解设置数据源。
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class DataSourceAspect {

    @Around("@annotation(dataSource)")
    public Object around(ProceedingJoinPoint point, DataSource dataSource) throws Throwable {
        try {
            DataSourceContextHolder.setDataSource(dataSource.value());
            return point.proceed();
        } finally {
            DataSourceContextHolder.clearDataSource();
        }
    }
}
  1. 使用注解切换数据源:在Service层方法上使用@DataSource注解,如:
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @DataSource("slave")
    public void readUser() {
        // 读操作,使用从库
    }

    @DataSource("master")
    public void writeUser() {
        // 写操作,使用主库
    }
}

可能遇到的问题和解决方案

  1. 事务管理问题:在多数据源环境下,事务管理需要特别注意。如果一个事务涉及多个数据源,需要使用分布式事务解决方案,如Seata。对于单数据源事务,可以使用Spring的@Transactional注解,确保事务在正确的数据源上进行。
  2. 性能问题:频繁切换数据源可能会带来性能开销。可以通过优化数据源连接池配置,减少连接创建和销毁的次数,以及合理设置缓存来缓解性能问题。
  3. 线程安全问题DataSourceContextHolder使用ThreadLocal来存储数据源,确保线程安全。但在使用线程池等多线程场景时,要注意数据源的正确传递和清理,避免线程间的数据污染。可以通过自定义线程池,在任务提交前设置数据源,任务完成后清理数据源。
  4. 数据源一致性问题:在读写分离场景下,可能会出现数据一致性问题,从库数据可能存在延迟。可以通过设置合适的缓存策略,或者在某些关键读操作时强制使用主库,来保证数据的一致性。