栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

springboot实现方法级别的动态多数据源切换

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

springboot实现方法级别的动态多数据源切换

springboot实现方法级别的动态多数据源切换
  • 原理概述
  • 具体实现
    • 环境搭建
    • 数据源配置
      • 在配置文件中配置数据库的连接信息
    • 实现AbstractRoutingDataSource抽象类
    • 替换mybaits中的sqlSessionFactory,以及事务管理器
    • 注解及切面的实现
  • 效果展示
  • 问题总结
  • 参考来源

原理概述

主要设计三大部分:druid连接池、spring内置的抽象类AbstractRoutingDataSource、注解及切面的配合使用。其中担任动态数据源切换重要角色的是AbstractRoutingDataSource的实现类,通过他我们可以设置默认的数据源以及多个待选数据源。

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
    @Nullable
    private Map targetDataSources;    //目标数据源的map,key是数据源的名称,value是配置的DataSource对象
    @Nullable
    private Object defaultTargetDataSource;   //默认数据源
    private boolean lenientFallback = true;
    private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
    @Nullable
    private Map resolvedDataSources;
    @Nullable
    private DataSource resolvedDefaultDataSource;
	...
	...

详细用法请接着往下看

具体实现 环境搭建

首先添加druid、mybatis等pom依赖、保证项目里有这些依赖就可以,其他基础依赖此处省略

        
        
            com.alibaba
            druid-spring-boot-starter
            1.1.21
        
        
            org.mybatis.spring.boot
            mybatis-spring-boot-starter
            2.1.2
        
        
            org.springframework.boot
            spring-boot-starter-aop
        
        
        
            mysql
            mysql-connector-java
            8.0.11
        
        
        
            com.alibaba
            fastjson
            1.2.70
        
数据源配置 在配置文件中配置数据库的连接信息
#mysql数据源
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    druid:
      filters: mergeStat
      initial-size: 0
      log-abandoned: true
      max-active: 20
      max-wait: 6000
      min-evictable-idle-time-millis: 25200000
      min-idle: 0
      remove-abandoned-timeout: 1800
      removeAbandoned: true
      test-on-borrow: false
      test-on-return: false
      test-while-idle: true
      time-between-eviction-runs-millis: 60000
      validation-query: SELECT 1
    password: xxxx
    type: com.alibaba.druid.pool.DruidDataSource
    url: jdbc:mysql://xxxx:3306/yuhang?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: xxxx
    red12hours:
      password: xxxx
      type: com.alibaba.druid.pool.DruidDataSource
      url: jdbc:mysql://xx.xx.xx.xx:3306/red_12_hours?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
      username: xxxx
      driverClassName: com.mysql.cj.jdbc.Driver
    publicweb:
      password: xxxxx
      type: com.alibaba.druid.pool.DruidDataSource
      url: jdbc:mysql://xx.xx.xx.xx:3306/publicweb?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
      username: xxxx
      driverClassName: com.mysql.cj.jdbc.Driver

此处共配置了三个数据源,以及其他的一些druid连接池的配置,写好yml配置文件后,需要编写springboot的Configuration类来创建对应的Bean存入IOC容器中。

@Configuration
public class DataSourcesConfig {

    
    @ConfigurationProperties(prefix = "spring.datasource")
    @Bean(name = "dataSource")
    @Primary
    public DataSource masterDataSource(){
        //做一些其他的自定义配置,比如密码加密等......
        return new DruidDataSource();
    }

    
    @ConfigurationProperties(prefix = "spring.datasource.red12hours")
    @Bean(name = "redDataSource")
    public DataSource redDataSource(){
        //做一些其他的自定义配置,比如密码加密等......
        return new DruidDataSource();
    }
        
    @ConfigurationProperties(prefix = "spring.datasource.publicweb")
    @Bean(name = "publicwebDataSource")
    public DataSource publicwebDataSource(){
        //做一些其他的自定义配置,比如密码加密等......
        return new DruidDataSource();
    }
 }

三个数据源则需要创建三个Bean对象,在@ConfigurationProperties注解中写好需要用的配置的前缀即可当做参数注入到新建的对象中,需要保证yml文件配置名和参数名的一致,写到这里还不算完,这个配置类后续还需要加入其他的配置Bean。

实现AbstractRoutingDataSource抽象类

因为druid连接池是公共的资源,不能因为这里的切换而影响了其他线程的执行,所以引入ThreadLocal来保证线程间的隔离

public class DataSourceHolder {

    //线程  本地环境
    private static final ThreadLocal dataSources = new InheritableThreadLocal();

    //设置数据源
    public static void setDataSource(String datasource) {
        dataSources.set(datasource);
    }

    //获取数据源
    public static String getDataSource() {
        return dataSources.get();
    }

    //清除数据源
    public static void clearDataSource() {
        dataSources.remove();
    }
}

因为ThreadLocal可以实现线程与线程之间的隔离,所以用ThreadLocal存储当前数据源的名称,每个线程都有其自己的当前数据源,所以单线程的切换不会影响到其他的线程,下面就要实现AbstractRoutingDataSource类了

public class DynamicDataSource extends AbstractRoutingDataSource {

    
    @Override
    protected Object determineCurrentLookupKey() {
        //从ThreadLocal中取出KEY
        return DataSourceHolder.getDataSource();
    }

    
    public DynamicDataSource(DataSource defaultTargetDataSource, Map targetDataSources) {
        //默认的数据源,可以作为主数据源
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        //目标数据源
        super.setTargetDataSources(targetDataSources);
        //执行afterPropertiesSet方法,完成属性的设置
        super.afterPropertiesSet();
    }
}

下一步,创建DynamicDataSourceBean,将多数据源注入其中,在上面编写的DataSourcesConfig配置类中加入下面的代码

    @Bean(name = "dynamicDataSource")
    public DynamicDataSource DynamicDataSource(
            @Autowired @Qualifier("dataSource") DataSource primary,
            @Autowired @Qualifier("publicwebDataSource") DataSource publicwebDataSource,
            @Autowired @Qualifier("redDataSource") DataSource redDataSource
    ){
        HashMap ds = new HashMap<>();
        ds.put("redDataSource",redDataSource);
        ds.put("publicwebDataSource",publicwebDataSource);
        //做一些其他的自定义配置,比如密码加密等......
        DynamicDataSource dynamicDataSource = new DynamicDataSource(primary,ds);
        return dynamicDataSource;
    }
替换mybaits中的sqlSessionFactory,以及事务管理器

注意入参是我们自己定义的实现了AbstractRoutingDataSource 类的多数据源管理对象

    
    @Primary
    @Bean("sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactoryBean(DynamicDataSource dynamicDataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dynamicDataSource);
        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.setDefaultFetchSize(100);
        configuration.setDefaultStatementTimeout(30);
        sqlSessionFactoryBean.setConfiguration(configuration);
        return sqlSessionFactoryBean.getObject();
    }
    
    @Primary
    @Bean(value = "transactionManager")
    public PlatformTransactionManager annotationDrivenTransactionManager(DynamicDataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

OK,截止到目前系统中已经具备了多数据源动态切换的能力,你可以 通过以下方式来实现数据源的切换,同时,以下方式可以实现同一方法内去执行不同的数据源查询,但是事务管理此时应该是不生效了,但是还没有实测。

    public List getAllData(){
        ArrayList jsonObjects = new ArrayList<>();
        //不指定数据源则使用默认的
        List sysUser = dsTestMapper.getSysUserList();
        DataSourceHolder.setDataSource("redDataSource");  //切换数据源
        List sysUser2 = dsTestMapper.getWxUser();
        DataSourceHolder.clearDataSource();  //使用完记得清除掉数据源,相当于切换回默认数据源
        jsonObjects.add(sysUser.get(0));
        jsonObjects.add(sysUser2.get(0));
        return jsonObjects;
    }

而实际项目中,较多的场景是方法内数据源一致,所以搭配注解来实现方法级别的数据源切换是非常常用的。

注解及切面的实现

编写注解:

@Target(value = ElementType.METHOD)
@Retention(value = RetentionPolicy.RUNTIME)
@Documented
public @interface SwitchSource {

    
    String DEFAULT_NAME = "redDataSource";

    
    String value() default DEFAULT_NAME;
}

对加注解了的方法添加环绕切面

@Aspect
//优先级要设置在事务切面执行之前
@Order(1)
@Component
@Slf4j
public class DataSourceAspect {


    @Pointcut("@annotation(SwitchSource)")
    public void pointcut() {
    }

    
    @Before(value = "pointcut()")
    public void beforeOpt(JoinPoint joinPoint) {
        
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        SwitchSource switchSource = method.getAnnotation(SwitchSource.class);
        log.info("[Switch DataSource]:" + switchSource.value());
        DataSourceHolder.setDataSource(switchSource.value());
    }

    
    @After(value = "pointcut()")
    public void afterOpt() {
        DataSourceHolder.clearDataSource();
        log.info("[Switch Default DataSource]");
    }
}
效果展示
    @RequestMapping(value = "getSysUser",method = RequestMethod.GET)
    @Transactional(rollbackFor = Exception.class)
    public List getSysUser(){
        List sysUser = dsTestMapper.getSysUserList();
        return sysUser;
    }

    @SwitchSource("redDataSource")
    @RequestMapping(value = "getWxUser",method = RequestMethod.GET)
    @Transactional(rollbackFor = Exception.class)
    public List getWxUser(){
        List sysUser = dsTestMapper.getWxUser();
        return sysUser;
    }

    @SwitchSource("publicwebDataSource")
    @RequestMapping(value = "getPublicDoc",method = RequestMethod.GET)
    @Transactional(rollbackFor = Exception.class)
    public List getPublicDoc(){
        List sysUser = dsTestMapper.getPublicDoc();
        return sysUser;
    }

问题总结

需要注意的一点是因为切换数据源是通过注解加切面实现的,所以就会出现以下问题:
A类中有m方法使用m数据源,n方法使用n数据源,同时在A类中有方法k来调用m,n方法,此时发现切换数据源是不生效的,都是使用的k方法的数据源,解决方法就是把k方法放到m和n所在类以外的其他类中,此类问题在使用注解加切面的编程中都会存在,例如SpringCache的使用。

参考来源

微信公众号:程序员DD,干货很多,此篇大多参考 link这边文章,并经过实践后,总结的使用方法,里面有很多原理、源码,可以详读。但是原博文里面缺少了一部分代码,就是创建DynamicDataSourceBean的那部分,没有那部分会导致项目启动报错

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/967176.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号