掘金 后端 ( ) • 2024-04-25 09:52

引用:https://blog.csdn.net/hc1285653662/article/details/124685728#comments_25870718 作者:后晨

引用:https://blog.csdn.net/z69183787/article/details/114096691 作者:Kevin_Darcy

引用:https://blog.csdn.net/Ajaxt/article/details/107401685 作者:可爱又迷人的少女杀手

Mybatis 多数据源配置

基于Mybatis配置多个数据源,实现在数据库操作时,对数据源的切换。

下面代码的目录结构:

image-20230404185009719.png

一、准备工作

  • 切面需要用到:(剩下都是正常mybatis,数据源,springboot正常的整合依赖)

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    
  • SQL语句:

    CREATE TABLE `person` (
      `id` int NOT NULL AUTO_INCREMENT,
      `name` varchar(64) DEFAULT NULL,
      `sex` char(4) DEFAULT NULL,
      KEY `id` (`id`)
    ) ENGINE=InnoDB;
    
  • 我这里准备了三个数据库,用不同的端口模拟多数据源:

    spring:
      datasource:
        username: root
        password: 123456
        url: jdbc:mysql://localhost:3306/yogurtlearn?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
    
        names : first,second
        first:
          type: com.alibaba.druid.pool.DruidDataSource
          driver-class-name : com.mysql.cj.jdbc.Driver
          url : jdbc:mysql://localhost:3307/first?useSSL=false&serverTimezone=UTC
          username : root
          password : 123456
        second:
          type: com.alibaba.druid.pool.DruidDataSource
          driver-class-name : com.mysql.cj.jdbc.Driver
          url : jdbc:mysql://localhost:3308/second?useSSL=false&serverTimezone=UTC
          username : root
          password : 123456
    

    分别在这三个库中都建立一个person表:

    insert into `person`(`id`,`name`,`sex`) values (0,'烤包子','男');    -> 在 3306/yogurtlearn 库中插入
    insert into `person`(`id`,`name`,`sex`) values (1,'小王','男');      -> 在 3307/first 库中插入
    insert into `person`(`id`,`name`,`sex`) values (2,'小李','女');      -> 在 3308/second 库中插入
    
  • Mapper、Entity

    @Mapper
    public interface DynamicDataSourceMapper {
        @Select("select * from person") // 什么都不配,走默认
        List<Person> defaultDbQuery1();
    
        @TargetDataSource(name = "default") // 走默认
        @Select("select * from person")
        List<Person> defaultDbQuery2();
    
        @TargetDataSource(name = "first") // first: 走localhost:3307/first
        @Select("select * from person")
        List<Person> firstCustomDataSourceQuery();
    
        @TargetDataSource(name = "second")
        @Select("select * from person")
        List<Person> secondCustomDataSourceQuery(); // second: 走localhost:3307/second
    }
    
    @Data
    public class Person{
        private Integer id;
        private String name;
        private String sex;
    }
    

二、注册多个数据源

  • DynamicDataSourceRegister

    作用:① 解析配置文件,读取多个数据源信息;② 多个数据源到Spring

    @Slf4j
    public class DynamicDataSourceRegister implements ImportBeanDefinitionRegistrar, EnvironmentAware {
        // 默认数据源(主数据源)
        private DataSource defaultDataSource;
        // 其他数据源  key:数据源名 value:数据源信息
        private Map<String,DataSource> customDataSources = new HashMap<>();
    
        @Override
        public void setEnvironment(Environment environment) {
            initDefaultDataSource(environment); // 构建主数据源
            initCustomDataSources(environment); // 初始化其他数据源
        }
    
        /**
         * 向Spring容器中注入动态数据源
         * @param importingClassMetadata
         * @param registry
         */
        @Override
        public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
            Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
            // 1. 将主数据源添加到更多数据源中
            targetDataSources.put("dataSource", defaultDataSource);
            DynamicDataSourceContextHolder.dataSourceIds.add("dataSource");
            // 2. 添加更多数据源
            targetDataSources.putAll(customDataSources);
            for (String key : customDataSources.keySet()) {
                DynamicDataSourceContextHolder.dataSourceIds.add(key);
            }
    
            // 创建DynamicDataSource
            GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
            beanDefinition.setBeanClass(DynamicDataSource.class);
            beanDefinition.setSynthetic(true);
            MutablePropertyValues mpv = beanDefinition.getPropertyValues();
            mpv.addPropertyValue("defaultTargetDataSource", defaultDataSource);
            mpv.addPropertyValue("targetDataSources", targetDataSources);
            registry.registerBeanDefinition("dataSource", beanDefinition);
        }
    
        /**
         * 构建主数据源
         */
        private void initDefaultDataSource(Environment env) {
            // 读取主数据源配置
            Map<String, Object> dsMap = new HashMap<>();
            dsMap.put("type", env.getProperty("spring.datasource.type"));
            dsMap.put("driver-class-name", env.getProperty("spring.datasource.driver-class-name"));
            dsMap.put("url", env.getProperty("spring.datasource.url"));
            dsMap.put("username", env.getProperty("spring.datasource.username"));
            dsMap.put("password", env.getProperty("spring.datasource.password"));
            // 构建DataSource数据源
            defaultDataSource = buildDataSource(dsMap);
            dataBinder(defaultDataSource, env);
        }
    
        /**
         * 初始化其他数据源
         * ★★★
         * 在这在获取其他数据源的时候,有两种方式:
         *   1. 在配置文件中配置好的,直接读取配置文件;(这里使用该种方式)
         *   2. 在默认的数据源数据库中来获取更多数据源。(可以有一个数据源配置表,配置数据源信息)
         */
        private void initCustomDataSources(Environment env) {
            String dataSourceNames = env.getProperty("spring.datasource.names");
            if (StringUtils.isNotBlank(dataSourceNames)) {
                for (String dsPrefix : dataSourceNames.split(",")) {// 多个数据源
                    Iterable<ConfigurationPropertySource> sources = ConfigurationPropertySources.get(env);
                    Binder binder = new Binder(sources);
                    BindResult<Properties> bindResult = binder.bind("spring.datasource." + dsPrefix, Properties.class);
                    Properties properties = bindResult.get();
                    Map<String, Object> dsMap = new HashMap<>();
                    dsMap.put("type", properties.getProperty("type"));
                    dsMap.put("driver-class-name", properties.getProperty("driver-class-name"));
                    dsMap.put("url", properties.getProperty("url"));
                    dsMap.put("username", properties.getProperty("username"));
                    dsMap.put("password", properties.getProperty("password"));
                    // 构建DataSource数据源
                    DataSource ds = buildDataSource(dsMap);
                    dataBinder(ds, env);
                    customDataSources.put(dsPrefix, ds);
                }
            }
        }
    
        /**
         * 利用读取的配置创建数据源
         * @param dsMap
         * @return
         */
        public DataSource buildDataSource(Map<String, Object> dsMap) {
            Object type = dsMap.get("type");
            Class<? extends DataSource> dataSourceType;
            try {
                dataSourceType = (Class<? extends DataSource>) Class.forName((String) type);
                String driverClassName = dsMap.get("driver-class-name").toString();
                String url = dsMap.get("url").toString();
                String username = dsMap.get("username").toString();
                String password = dsMap.get("password").toString();
                DataSource defaultDataSource = DataSourceBuilder.create()
                                                                .type(dataSourceType)
                                                                .driverClassName(driverClassName)
                                                                .url(url).username(username)
                                                                .password(password)
                                                                .build();
                return defaultDataSource;
            } catch (ClassNotFoundException e) {
                log.error("buildDataSource from config error!", e);
            }
            return null;
        }
    
        /**
         * 为数据源绑定更多属性
         * @param dataSource
         * @param env
         */
        private void dataBinder(DataSource dataSource, Environment env) {
        }
    }
    
  • DynamicDataSourceConfiguration:

    作用:必须要有,否则上面的 DynamicDataSourceRegister不会被注册。

    @Configuration
    @Import(DynamicDataSourceRegister.class)
    public class DynamicDataSourceConfiguration {
    }
    
  • DynamicDataSourceContextHolder

    作用:保存当前方法要使用的数据源名,并利用ThreadLocal实现数据源的线程隔离。

    /**
     * 动态数据源的切换主要是通过调用这个类的方法来完成的。
     * 在任何想要进行切换数据源的时候都可以通过调用这个类的方法实现切换。
     */
    public class DynamicDataSourceContextHolder {
        private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
        public static List<String> dataSourceIds = new ArrayList<>();
    
        public static void setDataSourceType(String dataSourceType) {
            contextHolder.set(dataSourceType);
        }
    
        public static String getDataSourceType() {
            return contextHolder.get();
        }
    
        public static void clearDataSourceType() {
            contextHolder.remove();
        }
    
        /**
         * 判断指定DataSource当前是否存在
         */
        public static boolean containsDataSource(String dataSourceId) {
            return dataSourceIds.contains(dataSourceId);
        }
    }
    
  • DynamicDataSource

    AbstractRoutingDataSource#determineCurrentLookupKey()就是在数据源调用 getConnetion() 方法时候,决定使用那个数据源提供的一个钩子方法。

    这里我们对其做重写,在获取数据源得到时候,从当前线程的ThreadLocal中获取到数据源名称,进而去取对应数据源信息。

    @Slf4j
    public class DynamicDataSource extends AbstractRoutingDataSource {
        /**
         * 动态数据库
         * 重写determineCurrentLookupKey方法,决定当前使用的数据源是哪一个
         */
        @Override
        protected Object determineCurrentLookupKey() {
            log.info("获取当前数据源:{}",DynamicDataSourceContextHolder.getDataSourceType());
            return DynamicDataSourceContextHolder.getDataSourceType();
        }
    }
    

    直接粘出来结论:AbstractRoutingDataSource#determineTargetDataSource()

    image-20230404172608214.png

三、实现数据源切换

  • 注解:作用在方法上,实现指定数据源名称

    /**
     * 在方法上使用,用于指定使用哪个数据源
     */
    @Target({ElementType.METHOD, ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface TargetDataSource {
        String name();
    }
    
  • AOP:

    当指定数据源的注解打在方法上时,由该切面进行拦截,然后利用 DynamicDataSourceContextHolder.setDataSourceType() 设置数据源的名称;之后再操作数据库时,获取的 getConnetion() 就是上面我们Debug AbstractRoutingDataSource#determineTargetDataSource(),利用设置的数据源名称获取指定数据源信息。最后在恢复现场,清除当前线程请求的连接信息。

    @Slf4j
    @Aspect
    @Component
    public class DataSourceAspect implements Ordered {
        @Pointcut("@annotation(com.transactional.dynamicdatasource.annotation.TargetDataSource)")
        public void dataSourcePointCut() {
        }
    
        @Around("dataSourcePointCut()")
        public Object around(ProceedingJoinPoint point) throws Throwable {
            // 1. 获取方法上的注解
            MethodSignature signature = (MethodSignature) point.getSignature();
            Method method = signature.getMethod();
            TargetDataSource ds = method.getAnnotation(TargetDataSource.class);
            // 2. 方法上打了 数据源注解,利用DynamicDataSourceContextHolder 将当前数据源切换成 ds.name()
            if (ds != null) {
                DynamicDataSourceContextHolder.setDataSourceType(ds.name());
                log.info("set datasource is " + ds.name());
            }
    
            // 3. 执行方法
            try {
                return point.proceed();
            } finally {
                // 4. 恢复现场,清除当前数据源
                DynamicDataSourceContextHolder.clearDataSourceType();
                log.debug("clean datasource");
            }
        }
    
        @Override
        public int getOrder() {
            return 1;
        }
    }
    

四、排除自动配置数据源

Spring Boot 在启动的时候,默认数据源会自动加载,所以这里在启动的时候要排除掉数据源自动的加载。

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class TransactionalApplication {
    public static void main(String[]  args) {
        SpringApplication.run(TransactionalApplication.class, args);
    }
}

五、测试

@RunWith(SpringRunner.class)
@SpringBootTest(classes = {TransactionalApplication.class})
public class DynamicDataSourceServiceImplTest {

    @Resource
    private DynamicDataSourceMapper dynamicDataSourceMapper;

    @Test
    public void defaultDbQuery2() {
        System.out.println(dynamicDataSourceMapper.defaultDbQuery1());
        System.out.println(dynamicDataSourceMapper.defaultDbQuery2());
        System.out.println(dynamicDataSourceMapper.firstCustomDataSourceQuery());
        System.out.println(dynamicDataSourceMapper.secondCustomDataSourceQuery());
    }
}

执行结果:

  • dynamicDataSourceMapper.defaultDbQuery1()

    没有在方法打注解,走默认的。

    org.springframework.jdbc.datasource.DataSourceUtils - Fetching JDBC Connection from DataSource
    com.transactional.dynamicdatasource.config.DynamicDataSource - 获取当前数据源:null
    com.alibaba.druid.pool.DruidDataSource - {dataSource-1} inited
    [Person(id=0, name=烤包子, sex=男)]
    
  • dynamicDataSourceMapper.defaultDbQuery2()

    在方法上打注解,指定的 name = default , 也是走默认的。

    com.transactional.dynamicdatasource.aspect.DataSourceAspect - set datasource is default
    org.springframework.jdbc.datasource.DataSourceUtils - Fetching JDBC Connection from DataSource
    com.transactional.dynamicdatasource.config.DynamicDataSource - 获取当前数据源:default
    [Person(id=0, name=烤包子, sex=男)]
    
  • dynamicDataSourceMapper.firstCustomDataSourceQuery()

    在方法上打注解,指定的 name = first,可以看到查询结果也是从 first.person 库中查询出来的数据。

    com.transactional.dynamicdatasource.aspect.DataSourceAspect - set datasource is first
    org.springframework.jdbc.datasource.DataSourceUtils - Fetching JDBC Connection from DataSource
    com.transactional.dynamicdatasource.config.DynamicDataSource - 获取当前数据源:first
    com.alibaba.druid.pool.DruidDataSource - {dataSource-2} inited
    [Person(id=1, name=小王, sex=男)]
    
  • dynamicDataSourceMapper.secondCustomDataSourceQuery()

    在方法上打注解,指定的 name = second,可以看到查询结果也是从 second.person 库中查询出来的数据。

    com.transactional.dynamicdatasource.aspect.DataSourceAspect - set datasource is second
    org.springframework.jdbc.datasource.DataSourceUtils - Fetching JDBC Connection from DataSource
    com.transactional.dynamicdatasource.config.DynamicDataSource - 获取当前数据源:second
    com.alibaba.druid.pool.DruidDataSource - {dataSource-3} inited
    [Person(id=2, name=小李, sex=女)]