springboot 基於註解實現多數據源切換

1 背景

    業務開發中,後端通常需要操作多個數據庫(可能同類型,也可能不同類型)中的數據,比如主、從數據庫的切換場景通常就是同類型切換。但實際需求中,也有可能需要不同類型數據庫之間的切換。不論是否同類型,其背後原理一致,只需在配置文件中修改數據庫驅動即可。
    springboot 提供的AbstractRoutingDataSource實現多數據源動態切換的核心邏輯是:通過AOP的方式在程序運行時,把數據源通過 AbstractRoutingDataSource 動態織入到程序中,靈活地進行數據源切換。本文記錄了利用AbstractRoutingDataSource實現在service層通過註解的方式對Mysql和Postgresql兩種數據庫動態切換。

2 基本配置及maven依賴

    首先在application.yml文件中添加數據源配置

spring:
  profiles:
    active: @pom.env@
  http:
    encoding:
      charset: utf-8
      force: true
      enabled: true

mybatis:
  mapper-locations: classpath:/mapper/*.xml
  type-aliases-package: com.cetiti.test.model

    application-dev.yml開發環境配置:

spring:
  datasource:
    db1:
      driver-class-name: org.postgresql.Driver
      jdbc-url: jdbc:postgresql://10.0.40.70:5432/common_account
      username: postgres
      password: 123456
    db2:
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://10.0.30.232:3306/test?useUnicode=true&characterEncoding=utf8&useSSL=false
      username: root
      password: 123456

    maven依賴:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>

    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.68</version>
    </dependency>
    
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>1.3.4</version>
    </dependency>

    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <scope>runtime</scope>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>

    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger2</artifactId>
        <version>2.9.2</version>
    </dependency>
    
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger-ui</artifactId>
        <version>2.9.2</version>
    </dependency>

    <dependency>
        <groupId>com.github.pagehelper</groupId>
        <artifactId>pagehelper-spring-boot-starter</artifactId>
        <version>1.2.3</version>
    </dependency>

    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.5</version>
    </dependency>

</dependencies>

3 代碼實現

3.1 編寫數據源配置類

    姑且命名爲:MyBatisConfig

@Configuration
public class MyBatisConfig {

    /**
     * @return
     * @throws Exception
     * @Primary 必需指定一個且只能有一個主數據源,否則報錯
     */
    @Primary
    @Bean("mysql")
    @ConfigurationProperties(prefix = "spring.datasource.db1")//根據數據源前綴到application.yml讀取數據源信息
    public DataSource masterDataSource() throws Exception {
        return DataSourceBuilder.create().build();
    }

    @Bean("postgresql")
    @ConfigurationProperties(prefix = "spring.datasource.db2")//根據數據源前綴到application.yml讀取數據源信息//可以配置更多數據源,到前提是application.yml中存在,而且也需要在枚舉類中添加枚舉類型
    public DataSource slaverDataSource() throws Exception {
        return DataSourceBuilder.create().build();
    }

    /**
     * @Qualifier 根據名稱進行注入,通常是在具有相同的多個類型的實例的一個注入(例如有多個DataSource類型的實例)
     * @DataSourceTypeAnno(DataSourceEnum.MASTER)事務方法需要指定數據源
     */
    @Bean("dynamicDataSource")
    public DynamicDataSource dynamicDataSource(@Qualifier("mysql") DataSource masterDataSource,
                                               @Qualifier("postgresql") DataSource slaverDataSource) {
        Map<Object, Object> targetDataSources = new HashMap<>(4);
        targetDataSources.put(DataSourceEnum.MYSQL, masterDataSource);
        targetDataSources.put(DataSourceEnum.POSTGRESQL, slaverDataSource);

        DynamicDataSource dataSource = new DynamicDataSource();
        // 該方法是AbstractRoutingDataSource的方法
        dataSource.setTargetDataSources(targetDataSources);
        // 默認的datasource設置爲myTestDbDataSource
        dataSource.setDefaultTargetDataSource(masterDataSource);
        return dataSource;
    }

    /**
     * 根據數據源創建SqlSessionFactory
     */
    @Bean
    public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DynamicDataSource dynamicDataSource,
                                               @Value("mybatis.type-aliases-package") String typeAliasesPackage) throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        // 指定數據源(這個必須有,否則報錯)
        factoryBean.setDataSource(dynamicDataSource);
        // 下邊兩句僅僅用於*.xml文件,如果整個持久層操作不需要使用到xml文件的話(只用註解就可以搞定),則不加
        // 指定實體類所在的包 //掃描mapper.xml文件包
        //factoryBean.setTypeAliasesPackage(typeAliasesPackage);
        //factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapping/**/*Mapper.xml"));
        return factoryBean.getObject();
    }

    /**
     * 配置事務管理器
     */
    @Bean
    public DataSourceTransactionManager transactionManager(DynamicDataSource dataSource) throws Exception {
        return new DataSourceTransactionManager(dataSource);
    }
}
3.2 數據源標識枚舉類:
public enum DataSourceEnum {
    // 主
    MYSQL,

    //備
    POSTGRESQL;
}
3.3 添加AbstractRoutingDataSource實現類

    後續在獲取數據源標識時會回調該類中的determineCurrentLookupKey方法:

public class DynamicDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
    	//自定義數據源標識上下文容器DataSourceContextHolder,用於存放各標識
        return DataSourceContextHolder.getDataSourceType();
    }
}

    通過追蹤源碼可見,AbstractRoutingDataSource的getConnection() 方法根據查找 lookupkey 鍵對不同目標數據源的調用,通常是通過某些線程綁定的事務上下文來實現。
    實現邏輯:
        定義DynamicDataSource類繼承抽象類AbstractRoutingDataSource,並實現了determineCurrentLookupKey()方法。
        把配置的多個數據源放在AbstractRoutingDataSource的 targetDataSources和defaultTargetDataSource中,然後通過afterPropertiesSet()方法將數據源分別進行復制到resolvedDataSources和resolvedDefaultDataSource中。
        調用AbstractRoutingDataSource的getConnection()的方法的時候,先調用determineTargetDataSource()方法返回DataSource,再進行getConnection(),determineTargetDataSource()源碼如下:

protected DataSource determineTargetDataSource() {
    Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
    //獲取目標數據源鍵值
    Object lookupKey = this.determineCurrentLookupKey();
    //根據鍵值,獲取數據源
    DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
    if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
        dataSource = this.resolvedDefaultDataSource;
    }
	//如果沒有獲取到,證明不存在,則獲取默認數據源
    if (dataSource == null) {
        throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
    } else {
        return dataSource;
    }
}
3.4 定義數據源標識上下文容器,

    本質上還是利用ThreadLocal實現數據安全

public class DataSourceContextHolder {

    private static final ThreadLocal<DataSourceEnum> CONTEXT_HOLDER = ThreadLocal.withInitial(() -> DataSourceEnum.MYSQL);

    public static void setDataSourceType(DataSourceEnum type) {
        CONTEXT_HOLDER.set(type);
    }

    public static DataSourceEnum getDataSourceType() {
        return CONTEXT_HOLDER.get();
    }

    public static void resetDataSourceType() {
        CONTEXT_HOLDER.set(DataSourceEnum.MYSQL);
    }
}
3.5 自定義註解:
@Retention(RetentionPolicy.RUNTIME)
// 註解可以用在方法上
@Target(ElementType.METHOD)
public @interface DataSourceTypeAnno {

    //使用方式在service層方法上添加@DataSourceTypeAnno(DataSourceEnum.數據源枚舉類型)用於指定所使用的數據源
    DataSourceEnum value() default DataSourceEnum.MYSQL;

}
3.6 定義切面類:
@Component
@Aspect
@Order(-100)
public class DataSourceAspect {

    //這裏掃描service層方法上的自定義註解,去判斷所使用的數據源類型,並動態切換數據源
    @Pointcut("execution(* com.cetiti.*.*..*(..)) " +
            "&& @annotation(com.cetiti.rm.common.annotation.DataSourceTypeAnno)")
    public void dataSourcePointcut() {
    }

    @Around("dataSourcePointcut()")
    public Object doAround(ProceedingJoinPoint pjp) {
        MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
        Method method = methodSignature.getMethod();
        DataSourceTypeAnno typeAnno = method.getAnnotation(DataSourceTypeAnno.class);
        DataSourceEnum sourceEnum = typeAnno.value();

        if (sourceEnum == DataSourceEnum.MYSQL) {
            DataSourceContextHolder.setDataSourceType(DataSourceEnum.MYSQL);
        } else if (sourceEnum == DataSourceEnum.POSTGRESQL) {
            DataSourceContextHolder.setDataSourceType(DataSourceEnum.POSTGRESQL);
        }

        Object result = null;
        try {
            result = pjp.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        } finally {
            DataSourceContextHolder.resetDataSourceType();
        }
        return result;
    }
}

4 測試

4.1 新建表

    在不同類型數據庫下新建表如下:
    MySQL
在這裏插入圖片描述
    PostgreSQL:
在這裏插入圖片描述

4.2 service層代碼
@Service
public class StudentServiceImpl implements StudentService {

    @Resource
    private MyDbTestMapper myDbTestMapper;

    @Override
    @DataSourceTypeAnno(value = DataSourceEnum.MYSQL)
    public List<Student> getMasterStudent() {
        return myDbTestMapper.getStudent();
    }

    @Override
    @DataSourceTypeAnno(value = DataSourceEnum.POSTGRESQL)
    public List<Student> getSlaveStudent() {
        return myDbTestMapper.getStudent();
    }

}
4.3 mapper層代碼:
@Mapper
public interface MyDbTestMapper {

    /**
     * 獲取任務狀態字典表信息
     * @return list
     */
    @Select("select s_id as id,s_name as enName,t_id as tid,\n"+
            "real_name as cnName,sex,address,enroll_score as enrollScore,tel from student order by enroll_score")
    List<Student> getStudent();

}
4.4 controller層代碼:
@GetMapping("/test/students")
@ApiOperation("2.1 獲取學生信息")
public List<Student> getMyStudent() {
    List<Student> list = studentService.getMasterStudent();
    System.out.println(list);

    list = studentService.getSlaveStudent();
    System.out.println(list);
    return list;
}

    在測試之前還需要在應用主類中添加mapper掃描

@SpringBootApplication
@MapperScan("com.cetiti.test.mapper")
public class RedmineBackendApplication {

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

}

    可能會遇到測試異常情況:
在這裏插入圖片描述
    這事由於mysql版本過低所致,更改至5.5.*以上即可。

正常測試結果:
在這裏插入圖片描述

    注:基於AbstractRoutingDataSource的多數據源動態切換,可以實現主備切換、讀寫分離,這麼做缺點也很明顯,無法動態的增加數據源。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章