提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

目录

前言

一、Springboot中实现多数据源的方法?

1. 静态数据源切换

2. 动态数据源切换(基于AOP)

二、使用AOP实现多数据源动态切换

1.引入依赖

2.在application.yml中配置多数据源

3. 数据源属性配置类

4. 数据源常量类

5. 自定义多数据源配置类

6. 利用ThreadLocal解决线程安全问题

7. 继承AbstractRoutingDataSource定义数据源标识

8. 定义多数据源注解DataSource

9. 定义DataSource注解切片

10. 在调用方法上增加注解

总结


前言

在当今的软件开发领域,许多应用需要访问多个数据源,以下是多数据源的一些重要使用场景:

1. 读写分离:在高流量应用中,为了提高性能和负载均衡,通常会采用读写分离策略。读操作和写操作被路由到不同的数据库,以减轻数据库服务器的压力。多数据源是实现这一策略的关键,它能够选择适当的数据源,以实现读写分离。

2. 数据迁移和同步:在数据迁移或数据同步操作中,需要同时连接到源数据库和目标数据库。多数据源可确保数据从一个源复制到另一个目标,或者实现不同数据库之间的数据同步。

3. 业务和技术隔离:有时一个应用程序需要同时访问多个数据库,每个数据库可能用于不同的业务或技术目的。多数据源可确保请求根据业务或技术要求路由到正确的数据源。

综上所述,多数据源在各种关键应用场景中发挥着重要作用,有助于确保数据的隔离、性能和安全性。在接下来的部分,我们将探讨Spring Boot中来实现多数据源,以满足这些复杂需求的方法。


一、Springboot中实现多数据源的方法

多数据源设置可以分为静态数据源切换和动态数据源切换两种方式:

1. 静态数据源切换

通常情况下,我们会为每个数据源配置独立的sessionFactory和DAO层代码(以Hibernate或MyBatis为例)。这种方式被称为静态数据源配置。

2. 动态数据源切换(基于AOP)

在动态数据源切换中,我们引入了面向切面编程(AOP)的概念。通过AOP,我们可以在运行时动态切换数据源,而不需要在代码中硬编码多个SessionFactory。这种基于AOP的动态数据源切换方式更加灵活、可维护,使系统更具扩展性。

二、使用AOP实现多数据源动态切换

1.引入依赖

我们使用druid数据库连接池,在pom.xml文件中引入druid的坐标:

<dependency>    
    <groupId>com.alibaba</groupId>    
    <artifactId>druid-spring-boot-starter</artifactId>    
    <version>1.2.11</version>
</dependency>

2.在application.yml中配置多数据源

配置如下:

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      master: #master数据源        
        url: jdbc:mysql://localhost:3306/hrs?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
        username: root
        password: 123456
      slave:  #slave数据源        
        url: jdbc:mysql://localhost:3306/hrs_slave?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
        username: root
        password: 123456
      # 初始连接数      
      initialSize: 5
      # 最小连接池数量      
      minIdle: 10
      # 最大连接池数量      
      maxActive: 20
      # 配置获取连接等待超时的时间      
      maxWait: 60000
      # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒              
      timeBetweenEvictionRunsMillis: 60000
      # 配置一个连接在池中最小生存的时间,单位是毫秒      
      minEvictableIdleTimeMillis: 300000
      # 配置一个连接在池中最大生存的时间,单位是毫秒      
      maxEvictableIdleTimeMillis: 900000
      # 配置检测连接是否有效      
      validationQuery: SELECT 1 FROM DUAL
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      statViewServlet:
        enabled: true
        url-pattern: /monitor/druid

3. 数据源属性配置类

@Configuration
public class DruidProperties {
    @Value("${spring.datasource.druid.initialSize}")
    private int initialSize;

    @Value("${spring.datasource.druid.minIdle}")
    private int minIdle;

    @Value("${spring.datasource.druid.maxActive}")
    private int maxActive;

    @Value("${spring.datasource.druid.maxWait}")
    private int maxWait;

    @Value("${spring.datasource.druid.timeBetweenEvictionRunsMillis}")
    private int timeBetweenEvictionRunsMillis;

    @Value("${spring.datasource.druid.minEvictableIdleTimeMillis}")
    private int minEvictableIdleTimeMillis;

    @Value("${spring.datasource.druid.maxEvictableIdleTimeMillis}")
    private int maxEvictableIdleTimeMillis;

    @Value("${spring.datasource.druid.validationQuery}")
    private String validationQuery;

    @Value("${spring.datasource.druid.testWhileIdle}")
    private boolean testWhileIdle;

    @Value("${spring.datasource.druid.testOnBorrow}")
    private boolean testOnBorrow;

    @Value("${spring.datasource.druid.testOnReturn}")
    private boolean testOnReturn;

    public DruidDataSource dataSource(DruidDataSource datasource) {
        /** 配置初始化大小、最小、最大 */        
        datasource.setInitialSize(initialSize);
        datasource.setMaxActive(maxActive);
        datasource.setMinIdle(minIdle);

        /** 配置获取连接等待超时的时间 */        
        datasource.setMaxWait(maxWait);

        /** 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */        
        datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);

        /** 配置一个连接在池中最小、最大生存的时间,单位是毫秒 */        
        datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
        datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis);

        /**         
        * 用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。         
        */        
        datasource.setValidationQuery(validationQuery);
        /** 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 */        
        datasource.setTestWhileIdle(testWhileIdle);
        /** 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */        
        datasource.setTestOnBorrow(testOnBorrow);
        /** 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */        
        datasource.setTestOnReturn(testOnReturn);
        return datasource;
    }
}

4. 数据源常量类

public interface DataSourceName {
    String DEFAULT_DATASOURCE_NAME="master";
    String OTHER_DATASOURCE_NAME="slave";
}

5. 自定义多数据源配置类

@Configuration
public class DruidConfig
{
    @Bean    
    @ConfigurationProperties("spring.datasource.druid.master")
    public DataSource masterDataSource(DruidProperties druidProperties)
    {
        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
        return druidProperties.dataSource(dataSource);
    }

    @Bean    
    @ConfigurationProperties("spring.datasource.druid.slave")
    {
        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
        return druidProperties.dataSource(dataSource);
    }

    @Bean(name = "dynamicDataSource")
    @Primary    
    public DynamicDataSource dataSource(DataSource masterDataSource, DataSource slaveDataSource)
    {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DataSourceName.DEFAULT_DATASOURCE_NAME, masterDataSource);
        targetDataSources.put(DataSourceName.OTHER_DATASOURCE_NAME, slaveDataSource);
        return new DynamicDataSource(masterDataSource, targetDataSources);
    }
}

6. 利用ThreadLocal解决线程安全问题

public class DynamicDataSourceContextHolder {
    private static final Logger LOGGER = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);

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

    /**     
    * 设置数据源的变量     
    */    
    public static void setDateSoureName(String dsName) {
        LOGGER.info("切换到{}数据源", dsName);
        CONTEXT_HOLDER.set(dsName);
    }

    /**     
    * 获得数据源的变量     
    */    
    public static String getDateSoureName() {
        return CONTEXT_HOLDER.get();
    }

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

7. 继承AbstractRoutingDataSource定义数据源标识

public class DynamicDataSource extends AbstractRoutingDataSource {
    public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }

    @Override    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDateSoureName();
    }
}

8. 定义多数据源注解DataSource

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})//作用在类和方法上
// @Inherited//当以后我们在定义一个作用于类的注解时候,如果希望该注解也作用于其子类,那么可以用@Inherited 来进行修饰。
public @interface DataSource {
    String value() default DataSourceName.DEFAULT_DATASOURCE_NAME;
}

9. 定义DataSource注解切片

在transaction interpter执行之前就把动态数据源配置好,所以在动态数据源的配置的AOP切片上加入Order(1),让其先执行即可。

@Aspect
@Order(1)
@Component
public class DataSourceAspect {

    private static final Logger LOGGER = LoggerFactory.getLogger(DataSourceAspect.class);

    //定义切点,拦截@DataSource标注的类和方法    
    @Pointcut("@annotation(com.example.demo.annotation.DataSource)")
    public void pointCut(){}

    @Around("pointCut()")
    public Object useDataSource(ProceedingJoinPoint pjp) throws Throwable{
        // 获取注解中数据源名称        
        // 1.获取当前方法        
        MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
        // 2.获取当前方法上@DataSource的值,若方法上不存在,查找类上的值        
        Method method = methodSignature.getMethod();
        DataSource dataSource = method.getAnnotation(DataSource.class);
        // 3.若注解不为空,获取值,并存入本地线程        
        if(dataSource!=null){
            String value = dataSource.value();
            //将值存入ThreadLocal中            
            LOGGER.info("切换数据源为{}", dataSource.value());
            DynamicDataSourceContextHolder.setDateSoureName(value);
        }
        //4.执行原方法,有返回值直接返回        
        try {
            return pjp.proceed();
        } finally {
            if(dataSource!=null) {
                // 销毁数据源 在执行方法之后                
                LOGGER.info("销毁数据源{}", dataSource.value());
                //5.移除本地线程的值                
                DynamicDataSourceContextHolder.removeDataSourceName();
            }
        }
    }

}

10. 在调用方法上增加注解

我们需要在方法上添加@DataSource("数据源名称")注解,这样就可以利用AOP实现动态切换了。

可以在service层方法上增加注解:

@Service
public class TbDeptServiceImpl implements TbDeptService {
    。。。
    @DataSource(value = DataSourceName.OTHER_DATASOURCE_NAME)
    @Override
    public PageVO<TbDept> pageListSlave(TbDeptQuery query) {
    
        List<TbDept> pageList = tbDeptMapper.pageList(query);
        int totalCount = tbDeptMapper.pageListCount(query);
    
        // result    
        PageVO<TbDept> result = new PageVO<>(pageList, query.getPage(), query.getSize(), totalCount);
        return result;
    }
    。。。
} 

也可以在mapper层方法上增加注解:

@Mapper
@Repository
public interface TbDeptMapper {
    。。。
    @DataSource(value = DataSourceName.OTHER_DATASOURCE_NAME)
    TbDept load(int id);
    。。。
}


总结

本文介绍了SpringBoot中,通过AOP实现多数据源动态切换的方法。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐