动态数据源

动态数据源

SpringBoot 配置多数据源并动态切换

Spring Boot 中的多数据源配置方案

SpringBoot 多数据源配置/连接两个数据库

介绍

在实际开发中,往往会出现一个服务连接多个数据库的需求,这时候就需要在项目中进行灵活切换数据源来完成多个数据库操作。多数据源可以理解为多数据库,甚至可以是多个不同类型的数据库,比如一个是MySql,一个是Oracle。随着项目的扩大,有时需要数据库的拆分或者引入另一个数据库,这时就需要配置多个数据源。

实现思路

DataSource是和线程绑定的,动态数据源的配置主要是通过继承AbstractRoutingDataSource类实现的,实现在AbstractRoutingDataSource类中的 protected Object determineCurrentLookupKey()方法来获取数据源,所以我们需要先创建一个多线程 线程数据隔离的类来存放DataSource,然后在determineCurrentLookupKey()方法中通过这个类获取当前线程的DataSource,在AbstractRoutingDataSource类中,DataSource是通过Key-value的方式保存的,我们可以通过ThreadLocal来保存Key,从而实现数据源的动态切换。

引入mysql和jdbcTemplate依赖

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.0.31</version>
</dependency>

数据源配置

spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/db1?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=root

spring.datasource.db2.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.db2.url=jdbc:mysql://localhost:3306/db2?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
spring.datasource.db2.username=root
spring.datasource.db2.password=root

mybatis.type-aliases-package=com.example.demo02.pojo.domain
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.configuration.map-underscore-to-camel-case=true

创建数据源枚举类

@Getter
public enum DataSourceEnum {

    DEFAULT_DATASOURCE("defaultDataSource"),
    DB2_DATASOURCE("db2DataSource");

    String dataSourName;

    DataSourceEnum(String dataSourName) {
        this.dataSourName = dataSourName;
    }
}

数据源切换处理

public class DynamicDataSourceContextHolder {

    /**
     * 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
     * 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
     */
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

    /**
     * 设置数据源
     *
     * @param dataSourceType
     */
    public static void setDataSourceType(String dataSourceType) {
        System.out.printf("切换到{%s}数据源", dataSourceType);
        CONTEXT_HOLDER.set(dataSourceType);
    }

    /**
     * 获取数据源变量
     *
     * @return
     */
    public static String getDataSourceType() {
        return CONTEXT_HOLDER.get();
    }

    /**
     * 清空数据源变量
     */
    public static void clearDataSourceType() {
        CONTEXT_HOLDER.remove();
    }
}

继承AbstractRoutingDataSource

/**
 * 类描述:动态切换数据源主要依靠AbstractRoutingDataSource。
 * 创建一个AbstractRoutingDataSource的子类,重写determineCurrentLookupKey方法,用于决定使用哪一个数据源。
 * 这里主要用到AbstractRoutingDataSource的两个属性defaultTargetDataSource和targetDataSources。
 * defaultTargetDataSource默认目标数据源,targetDataSources(map类型)存放用来切换的数据源。
 */
public class DynamicDataSource extends AbstractRoutingDataSource {

    public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        super.setTargetDataSources(targetDataSources);
        // afterPropertiesSet()方法调用时用来将targetDataSources的属性写入resolvedDataSources中的
        super.afterPropertiesSet();
    }

    /**
     * 根据Key获取数据源的信息
     *
     * @return
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDataSourceType();
    }
}

注入数据源

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * 多数据源配置-注入数据源
 */
@Configuration
public class DataSourceConfig {

    /**
     * 默认数据源
     *
     * @return
     */
    @Bean("defaultDataSource")
    @Primary
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource defaultDataSource() {
        return DataSourceBuilder.create().build();
    }

    /**
     * db2数据库数据源
     *
     * @return
     */
    @Bean("db2DataSource")
    @ConfigurationProperties(prefix = "spring.datasource.db2")
    public DataSource db2DataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "dataSource")
    @Primary
    public DynamicDataSource dataSource(@Qualifier("defaultDataSource") DataSource defaultDataSource,
                                        @Qualifier("db2DataSource") DataSource db2DataSource) {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DataSourceEnum.DEFAULT_DATASOURCE.getDataSourName(), defaultDataSource);
        targetDataSources.put(DataSourceEnum.DB2_DATASOURCE.getDataSourName(), db2DataSource);
        return new DynamicDataSource(defaultDataSource, targetDataSources);
    }

    // TODO 测试数据源是否可用,其实就是使用当前数据源建立下连接,如果可以建立连接说明可用

    // TODO 配置数据源的其他连接池配置

}

自定义多数据源切换注解

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {

    DataSourceEnum value() default DataSourceEnum.DEFAULT_DATASOURCE;
}

AOP拦截类的实现

通过AOP在执行sql语句前拦截,并切换到自定义注解指定的数据源上。有一点需要注意,自定义数据源注解与 @Transaction 注解同一个方法时会先执行 @Transaction ,即获取数据源在切换数据源之前,所以会导致自定义注解失效,因此需要使用 @Order (@Order的value越小,就越先执行),保证该AOP在 @Transactional 之前执行。

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

/**
 * 类描述:通过拦截自定义的@DataSource注解,在其执行之前处理设置当前执行SQL的数据源的信息
 * CONTEXT_HOLDER.set(dataSourceType)这里的数据源信息从我们设置的注解上面获取信息,如果没有设置就是用默认的数据源的信息。
 *
 */
@Aspect
@Component
@Order(-1)
public class DataSourceAspect {

    /**
     * 作用Mapper接口
     */
    @Pointcut("execution(* com.example.demo02.mapper..*.*(..))")
    public void dsPointCut() {

    }

    @Before("dsPointCut()")
    public void changeDataSource(JoinPoint joinPoint) {
        boolean datasource = false;
        DataSource targetDataSource = null;
        for (Class clazz : joinPoint.getTarget().getClass().getInterfaces()) {
            datasource = clazz.isAnnotationPresent(DataSource.class);
            if (datasource) {
                targetDataSource = (DataSource) clazz.getAnnotation(DataSource.class);
                break;
            }
        }
        if (datasource) {
            String dataSource = targetDataSource.value().getDataSourName();
            // 数据源不存在则抛出异常
            if (!DynamicDataSourceContextHolder.containsDataSource(dataSource)) {
                log.error("datasource[{}] does not exist, will use default data source > {}"
                        , targetDataSource.value(), joinPoint.getSignature());
            } else {
                log.debug("Use DataSource: {} > {}", dataSource, joinPoint.getSignature());
                DynamicDataSourceContextHolder.setDataSourceType(dataSource);
            }
        } else {
            // 使用默认数据源
            DynamicDataSourceContextHolder.setDataSourceType(DataSourceEnum.DEFAULT_DATASOURCE.getDataSourName());
        }
    }

    @After("@annotation(targetDataSource)")
    public void restoreDataSource(JoinPoint joinPoint, DataSource targetDataSource) {
        log.debug("Revert DataSource: {} > {}", targetDataSource.value(), joinPoint.getSignature());
        DynamicDataSourceContextHolder.clearDataSourceType();
    }
}

启动类

需要在启动类的 @SpringBootApplication 注解中移除DataSource自动配置类,否则会默认自动配置,而不会使用我们自定义的DataSource,并且启动会有循环依赖的错误。

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@MapperScan(basePackages = "com.example.demo02.mapper")
public class Demo02Application {

    public static void main(String[] args) {
        SpringApplication.run(Demo02Application.class, args);
    }

}

mybatis配置

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.DependsOn;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;

/**
 * Mybatis配置
 */
@Component
@DependsOn("dataSource")
@MapperScan(basePackages = {"com.example.demo02.mapper"}, sqlSessionFactoryRef = "sqlSessionFactory")
public class MybatisConfig {

    @Value("${mybatis.mapper-locations}")
    private String mapperLocation;

    @Bean("sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory(@Qualifier("dataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        sqlSessionFactoryBean.setMapperLocations(
            new PathMatchingResourcePatternResolver().getResources(mapperLocation));

        return sqlSessionFactoryBean.getObject();
    }

    @Bean("sqlSessionTemplate")
    public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

    @Bean("transactionManager")
    public DataSourceTransactionManager transactionManager(@Qualifier("dataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

🌰测试数据源切换

  • mapper
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

/**
 * @author leizi
 * @create 2022-07-17 19:27
 */
@Repository
public interface StudentMapper {
    /**
     * 根据id获取名字
     *
     * @param id
     * @return
     */
    String getNameById(@Param("id") int id);
}
  • service
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @author leizi
 * @create 2022-07-17 19:26
 */
@Service
public class StudentServiceImpl implements StudentService {

    @Autowired
    private StudentMapper studentMapper;

    @Autowired
    private StudentDB2Mapper studentDB2Mapper;

    @Override
    public String getNameById(int id) {
        return studentMapper.getNameById(id);
    }

    @Override
    public String getUserNameByIdDB2(int id) {

        return studentDB2Mapper.getUserNameByIdDB2(id);
    }
}
  • controller
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author leizi
 * @create 2022-07-17 19:31
 */
@RestController
@RequestMapping("/student")
public class StudentController {

    @Autowired
    private StudentService studentService;


    @GetMapping("/get/{id}")
    public RestResult getNameById(@PathVariable("id") int id) {
        String name = studentService.getNameById(id);

        return RestResult.successResult(name);
    }

    @GetMapping("/get-db2/{id}")
    public RestResult getUserNameByIdDB2(@PathVariable("id") int id) {
        String name = studentService.getUserNameByIdDB2(id);
        return RestResult.successResult(name);
    }
}

遇到问题

Springboot报错:Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required

配置数据源多配置@Primary导致找不到主数据源而不能创建mybatis相关bean

Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'studentMapper' defined in file [F:\code\demo02\target\classes\com\example\demo02\mapper\StudentMapper.class]: Invocation of init method failed; nested exception is java.lang.IllegalArgumentException: Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required

posted @ 2023-02-26 11:27  Lz_蚂蚱  阅读(213)  评论(0)    收藏  举报