上文说到,springboot+mybatis项目配置多个数据源,但是整个dao类下的所有方法(甚至是整个包下)都指向同一个数据源,不够灵活。如果存在这样的场景,项目中有多个数据库,且是读写分离的,在一个dao类中有CRUD方法,CUD方法我们希望操作的是主库,读方法我们希望操作从库,也就是说在方法上指定操作的数据源,这样就需要配置动态数据源。

AbstractRoutingDataSource

Spring boot提供了AbstractRoutingDataSource 根据用户定义的规则选择当前的数据源,这样我们可以在执行查询之前,设置使用的数据源。实现可动态路由的数据源,在每次数据库查询操作前执行。它的抽象方法 determineCurrentLookupKey() 决定使用哪个数据源。
AbstractRoutingDataSource的多数据源动态切换的核心逻辑是:在程序运行时,把数据源数据源通过 AbstractRoutingDataSource 动态织入到程序中,灵活的进行数据源切换。
基于AbstractRoutingDataSource的多数据源动态切换,可以实现读写分离,这么做缺点也很明显,无法动态的增加数据源。

实现逻辑:

  1. 定义DynamicDataSource类继承抽象类AbstractRoutingDataSource,并实现了determineCurrentLookupKey()方法。
  2. 把配置的多个数据源会放在AbstractRoutingDataSource的 targetDataSources和defaultTargetDataSource中,然后通过afterPropertiesSet()方法将数据源分别进行复制到resolvedDataSources和resolvedDefaultDataSource中。
  3. 调用AbstractRoutingDataSource的getConnection()的方法的时候,先调用determineTargetDataSource()方法返回DataSource在进行getConnection()。

测试代码

springboot整合mybatis不赘述了,默认搭建好
Demo代码结构一览:
在这里插入图片描述

首先application.yml中配置多个数据源的数据

spring:
  datasource:
    db1:
      driverClassName: com.mysql.jdbc.Driver
      jdbcUrl: jdbc:mysql://192.168.233.136:3306/mydata?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf8&allowMultiQueries=true
      username: root
      password: 123456
    db2:
      driverClassName: com.mysql.jdbc.Driver
      jdbcUrl: jdbc:mysql://192.168.233.141:33306/today_top_news?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf8&allowMultiQueries=true
      username: root
      password: 123456

启动类也无需特殊处理

@SpringBootApplication
@MapperScan("com.meng.dao")
public class DynamicDbApplication {
    public static void main(String[] args) {
        SpringApplication.run(DynamicDbApplication.class  , args);
    }
}

pom.xml

<properties>
        <spring-boot.version>2.3.12.RELEASE</spring-boot.version>
    </properties>

    <dependencies>
        <!--注意:由于 spring-boot-starter-web 默认替我们引入了核心启动器 spring-boot-starter,
        因此,当 Spring Boot  项目中的 pom.xml 引入了 spring-boot-starter-web 的依赖后,
        就无须在引入 spring-boot-starter 核心启动器的依赖了-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>${spring-boot.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <version>${spring-boot.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
            <version>${spring-boot.version}</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.6</version>
        </dependency>



        <!--整合mytais-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.3</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.6</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.20</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.16</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>

springboot项目要想使用AOP需要加spring-boot-starter-aop、aspectjweaver这两个依赖

新建类DynamicDataSource,继承AbstractRoutingDataSource

/**
 * 扩展 Spring 的 AbstractRoutingDataSource 抽象类,重写 determineCurrentLookupKey 方法
 * 动态数据源
 * determineCurrentLookupKey() 方法决定使用哪个数据源
 */
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {

    /**
     * ThreadLocal 用于提供线程局部变量,在多线程环境可以保证各个线程里的变量独立于其它线程里的变量。
     * 也就是说 ThreadLocal 可以为每个线程创建一个【单独的变量副本】,相当于线程的 private static 类型变量。
     */
    public static final ThreadLocal<String> holder = new ThreadLocal<>();

    @Override
    protected Object determineCurrentLookupKey() {
        return getDataSource();
    }

    public static void setDataSource(String type) {
        holder.set(type);
    }

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

    public static void removeDataSource(){
        holder.remove();
    }
}

定义一个DataSourceConfig类

@Configuration
public class DataSourceConfig {

    /**
     * 定义两个数据源,读取application.yml中配置
     * @return
     */
    @Bean(name = "db1")
    @ConfigurationProperties(prefix = "spring.datasource.db1") // application.yml中对应属性的前缀
    public DataSource dataSource1() {
        return DataSourceBuilder.create().build();
    }
    @Bean(name = "db2")
    @ConfigurationProperties(prefix = "spring.datasource.db2") // application.yml中对应属性的前缀
    public DataSource dataSource2() {
        return DataSourceBuilder.create().build();
    }

    /**
     * 配置自定义的类DynamicDataSource,这里是把两个(多个)数据源放到DynamicDataSource中,
     * 同时设置一个默认的数据源
     */
    @Bean("dynamicDataSource")
    public DynamicDataSource dynamicDataSource(@Qualifier("db1")DataSource db1,
                                               @Qualifier("db2")DataSource db2){
        Map<Object,Object> targetDataSource = new HashMap<>();
        targetDataSource.put("db1",db1);
        targetDataSource.put("db2",db2);
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        dynamicDataSource.setTargetDataSources(targetDataSource);
        dynamicDataSource.setDefaultTargetDataSource(db1);
        return dynamicDataSource;
    }

    /**
     * 把动态数据源放到SqlSessionFactory
     * 同时配置扫描的mapping.xml
     */
    @Bean
    public SqlSessionFactory sqlSessionFactory(@Qualifier("db1")DataSource db1,
                                               @Qualifier("db2")DataSource db2) throws Exception{
        SqlSessionFactoryBean sqlSessionFactoryBean =  new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dynamicDataSource(db1,db2));
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));
        return sqlSessionFactoryBean.getObject();
    }

这个类中创建了两个数据源,和一个动态数据源,把两个数据源放到动态数据源中,同时指定一个默认的数据源,那么如果要切换数据源,只要通知系统使用DynamicDataSource 中的其他数据源即可,我们可以选择使用AOP实现

自定义注解DynamicDB

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DynamicDB {
    String value() default "";
}

写一个切面类DynamicDataSourceAspect,用于拦截方法,切换数据源

@Aspect
@Component
public class DynamicDataSourceAspect {
    
    @Pointcut("execution(* com.meng.dao.*.*(..))")
    public void pointcut(){}

    @Around("pointcut()")
    public Object around(ProceedingJoinPoint joinPoint)throws Throwable{
        System.out.println("-------------> selected dataSource aspect ");
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        DynamicDB dynamicDB = method.getAnnotation(DynamicDB.class);
        if(dynamicDB != null){
            String value = dynamicDB.value();
            //切换成指定的数据源
            DynamicDataSource.setDataSource(value);
        }
        try {
            return joinPoint.proceed();
        }finally {
            //操作完成,移除指定数据源,还原默认数据源
            DynamicDataSource.removeDataSource();
        }
    }

将需要切换数据源的方法添加自定义注解

@Repository
public interface ResultDao {
    
    @DynamicDB("db2")
    Result selectByPrimaryKey(Long id);

    int insertSelective(Result record);

    int deleteByPrimaryKey(Long id);

    int insert(Result record);

这样就完成了数据源的动态切换
注意,在开发中,这个注解应该要作用在service中的方法上,这里简单测试,就放在dao了

测试类

@SpringBootTest(classes = DynamicDbApplication.class)
@RunWith(SpringRunner.class)
public class TestDemo01 {

    @Autowired
    private ResultDao dao;

    @Test
    public void test02(){
        Result Result = dao.selectByPrimaryKey(2l);
        System.out.println("Result = " + Result);
    }
}

关于事务

AbstractRoutingDataSource 只支持单库事务,也就是说切换数据源要在开启事务之前执行。 spring DataSourceTransactionManager进行事务管理,开启事务,会将数据源缓存到DataSourceTransactionObject对象中进行后续的commit rollback等事务操作。

参考文章:
spring boot使用AbstractRoutingDataSource实现动态数据源切换
SpringBoot+MyBatis 配置多数据源 (两种方式实现)
.
.
.
.
.
.
.
.
.
.
.
.

(彩蛋)DynamicDataSourceAspect还可以这样写

@Aspect
@Component
public class DynamicDataSourceAspect {

    @Before("@annotation(DynamicDB)")
    public void beforeSwitchDS(JoinPoint point){
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        DynamicDB dynamicDB = method.getAnnotation(DynamicDB.class);
        if(dynamicDB != null){
            String value = dynamicDB.value();
            //切换成指定的数据源
            DynamicDataSource.setDataSource(value);
        }
    }
    @After("@annotation(DynamicDB)")
    public void afterSwitchDS(JoinPoint point){
        //操作完成,移除指定数据源,还原默认数据源
        DynamicDataSource.removeDataSource();
    }

@Before(“@annotation(DynamicDB)”),这种写法不需要切入点表达式了,我还真是头一次见,以后可以研究研究

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐