美文网首页
SpringBoot2+Mybatis多数据源切换和动态增减

SpringBoot2+Mybatis多数据源切换和动态增减

作者: zenghi | 来源:发表于2019-05-24 20:24 被阅读0次

    MyBatis多数据源切换

    项目结构为:

    图片.png

    项目相关依赖pom.xml

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.0.1</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    

    1、配置文件application.yml编辑

    spring:
      datasource:
        db1:
          driver-class-name: com.mysql.jdbc.Driver
          jdbc-url: jdbc:mysql://localhost:3306/cache?useSSL=FALSE&serverTimezone=UTC
          username: root
          password: root
        db2:
          driver-class-name: com.mysql.jdbc.Driver
          jdbc-url: jdbc:mysql://localhost:3306/test?useSSL=FALSE&serverTimezone=UTC
          username: root
          password: root
    # 这里不用配置mybatis的xml位置,在mybatis多数据源配置类中进行配置
    #mybatis:
    #  mapper-locations:
    #    - classpath:mapper/db1/*.xml     
    #    - classpath:mapper/db2/*.xml
    

    2、创建枚举类DataSourceType

    /**
     * @author Hayson
     * @description 列出所有数据源
     */
    public enum DataSourceType {
        db1,
        db2
    }
    

    3、创建动态数据源上下文

    /**
     * @author Hayson
     * @description 动态数据源上下文管理:设置数据源,获取数据源,清除数据源
     */
    public class DataSourceContextHolder {
        // 存放当前线程使用的数据源类型
        private static final ThreadLocal<DataSourceType> contextHolder = new ThreadLocal<>();
    
        // 设置数据源
        public static void setDataSource(DataSourceType type){
            contextHolder.set(type);
        }
    
        // 获取数据源
        public static DataSourceType getDataSource(){
            return contextHolder.get();
        }
    
        // 清除数据源
        public static void clearDataSource(){
            contextHolder.remove();
        }
    }
    

    4、动态数据源

    /**
     * @author Hayson
     * @description 动态数据源,每执行一次数据库,动态获取数据源
     */
    public class DynamicDataSource extends AbstractRoutingDataSource {
        @Override
        protected Object determineCurrentLookupKey() {
            return DataSourceContextHolder.getDataSource();
        }
    }
    

    5、Mybatis多数据源配置

    /**
     * @author Hayson
     * @description
     */
    @Configuration
    @MapperScan(basePackages = "com.example.multipledatabase.mapper")
    public class MybatisConfig {
        @Bean("db1DataSource")
        @Primary
        @ConfigurationProperties(prefix = "spring.datasource.db1")
        public DataSource db1DataSource() {
            DataSource build = DataSourceBuilder.create().build();
            return build;
        }
    
        @Bean("db2DataSource")
        @ConfigurationProperties(prefix = "spring.datasource.db2")
        public DataSource db2DataSource(){
            return DataSourceBuilder.create().build();
        }
    
        @Bean
        public DynamicDataSource dataSource(@Qualifier("db1DataSource") DataSource db1DataSource,
                                            @Qualifier("db2DataSource") DataSource db2DataSource) {
            Map<Object, Object> map = new HashMap<>();
            map.put(DataSourceType.db1, db1DataSource);
            map.put(DataSourceType.db2, db2DataSource);
    
            DynamicDataSource dynamicDataSource = new DynamicDataSource();
            dynamicDataSource.setTargetDataSources(map);
            dynamicDataSource.setDefaultTargetDataSource(db1DataSource);
    
            return dynamicDataSource;
        }
    
        @Bean
        public SqlSessionFactory sqlSessionFactory(DynamicDataSource dynamicDataSource) throws Exception {
            SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
            factoryBean.setDataSource(dynamicDataSource);
    //        factoryBean.setTypeAliasesPackage();
            // 设置mapper.xml的位置路径
            Resource[] resources = new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*/*.xml");
            factoryBean.setMapperLocations(resources);
            return factoryBean.getObject();
        }
    
        @Bean
        public PlatformTransactionManager transactionManager(DynamicDataSource dynamicDataSource){
            return new DataSourceTransactionManager(dynamicDataSource);
        }
    }
    

    6、自定义注解

    /*
     * @author Hayson
     * @description 自定义注解,用于类或方法上,优先级:方法>类
     */
    @Target({ElementType.METHOD, ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface DataSource {
        DataSourceType value() default DataSourceType.db1;
    }
    

    7、AOP切面设置数据源

    @Slf4j
    @Aspect
    @Component
    public class DataSourceAspect {
        @Before("@annotation(ds)")
        public void beforeDataSource(DataSource ds) {
            DataSourceType value = ds.value();
            DataSourceContextHolder.setDataSource(value);
            log.info("当前使用的数据源为:{}", value);
        }
        @After("@annotation(ds)")
        public void afterDataSource(DataSource ds){
            DataSourceContextHolder.clearDataSource();
        }
    }
    

    上面代码完后,即可以在Mybatismapper接口方法添加注解

    @Repository
    public interface GroupMapper {
        @DataSource(value = DataSourceType.db2)
        Map<String, Object> selectGroup();
    }
    

    service方法上添加注解:

    @Service
    @RequiredArgsConstructor
    public class UserService {
        private final UserMapper userMapper;
        private final GroupMapper groupMapper;
    
        public Map<String, Object> getUser(int id) {
            return userMapper.selectUser(id);
        }
    
        @DataSource(value = DataSourceType.db2)
        //@Transactional(rollbackFor = Exception.class)  // 如果需要事务,可添加
        public Map<String, Object> getUser2() {
            return groupMapper.selectGroup();
        }
    }
    

    上面的多数据源配置和切换已经完成,可实现在service层或mapper接口中添加注解@DataSource指定使用数据源,并且能实现单数据源的事务回滚。

    MyBatis运行期动态增减数据源

    我们知道,在项目程序启动时,就会加载所有的配置文件信息,就会读取到配置文件中所有的数据源配置,像上面的多数据源,在启动时,就读取了两种数据源配置,在请求执行时,从两个数据源中选择指定一个去连接数据库。

    而我目前负责的Bi项目中,就有数据库表中维护了所有客户的数据源,客户通过数据库的数据源连接到客户的数据库进行可视化数据分析。所以便想到通过在程序运行中,通过从数据库中获取数据源后,通过mybatis进行数据查询,避免通过原生JDBC进行查询,也方便SQL的管理。

    从上面多数据源配置切换中,知道需要继承AbstractRoutingDataSource类,必须指定一个数据源:

    图片

    简单分析一下AbstractRoutingDataSource抽象类的部分源码:

    public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
        private Map<Object, Object> targetDataSources;
        private Object defaultTargetDataSource;
    
        private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
        
        private Map<Object, DataSource> resolvedDataSources;
        private DataSource resolvedDefaultDataSource;
    
        ... // 省略getter/setter
    
        public void afterPropertiesSet() {
            if (this.targetDataSources == null) {
                throw new IllegalArgumentException("Property 'targetDataSources' is required");
            } else {
                this.resolvedDataSources = new HashMap(this.targetDataSources.size());
                this.targetDataSources.forEach((key, value) -> {
                    Object lookupKey = this.resolveSpecifiedLookupKey(key);
                    DataSource dataSource = this.resolveSpecifiedDataSource(value);
                    this.resolvedDataSources.put(lookupKey, dataSource);
                });
                if (this.defaultTargetDataSource != null) {
                    this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);
                }
    
            }
        }
    
        ... // 省略
    
        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;
            }
            ...
        }
    
        @Nullable
        protected abstract Object determineCurrentLookupKey();
    }
    

    对于该抽象类,关注两组变量:

    • Map<Object, Object> targetDataSourcesObject defaultTargetDataSource
    • Map<Object, DataSource> resolvedDataSourcesDataSource resolvedDefaultDataSource

    这两组变量是相互对应的。在熟悉多实例数据源切换代码的不难发现,当有多个数据源的时候,一定要指定一个作为默认的数据源,所以,当同时初始化多个数据源的时候,需要调用setDefaultTargetDataSource方法指定一个作为默认数据源;

    我们需要关注的是Map<Object, Object> targetDataSourcesMap<Object, DataSource> resolvedDataSourcestargetDataSources是暴露给外部程序用来赋值的,而resolvedDataSources是程序内部执行时的依据,因此会有一个赋值的操作,如下图所示:

    图片

    每次执行时,都会遍历targetDataSources内的所有元素并赋值给resolvedDataSources;这样如果我们在外部程序新增一个新的数据源,都会添加到内部使用,从而实现数据源的动态加载。

    该抽象类有一个抽象方法:protected abstract Object determineCurrentLookupKey(),该方法用于指定到底需要使用哪一个数据源:

    图片

    了解上面两段源码后,可以进行多数据源切换代码改造:

    1. 修改DynamicDataSource

      public class DynamicDataSource extends AbstractRoutingDataSource {
          private static Map<Object,Object> dataSourceMap=new HashMap<Object, Object>();
          private static DynamicDataSource instance;
          private static byte[] lock=new byte[0];
          
          // 重写setTargetDataSources,通过入参targetDataSources进行数据源的添加
          @Override
          public void setTargetDataSources(Map<Object, Object> targetDataSources) {
              super.setTargetDataSources(targetDataSources);
              dataSourceMap.putAll(targetDataSources);
              super.afterPropertiesSet();
          }
      
          // 单例模式,保证获取到都是同一个对象,
          public static synchronized DynamicDataSource getInstance(){
              if(instance==null){
                  synchronized (lock){
                      if(instance==null){
                          instance=new DynamicDataSource();
                      }
                  }
              }
              return instance;
          }
      
          @Override
          protected Object determineCurrentLookupKey() {
              return DataSourceContextHolder.getDataSource();
          }
      
          // 获取到原有的多数据源,并从该数据源基础上添加一个或多个数据源后,
          // 通过上面的setTargetDataSources进行加载
          public Map<Object, Object> getDataSourceMap() {
              return dataSourceMap;
          }
      }
      
    2. 修改数据源类型枚举,之前是如下:

      public enum DataSourceType {
          db1,
          db2
      }
      

      所以多数据源的配置类型指定为DataSourceType

      • DataSourceContextHolder

        图片
    • MyBatisDataSourceConfig

      图片
    • DataSourceAspect

      图片

      之前使用枚举类型进行配置,因为是固定了只有db1db2,所以可以统一指定了使用枚举类型,而现在进行动态添加数据源,因为从数据库获取到数据源,以该数据源的id作为数据源的key,所以统一使用String类型的Key

    • 修改枚举类DataSourceType

      public enum DataSourceType {
          db1("db1"),
          db2("db2");
          private String db;
          DataSourceType(String db) {
              this.db = db;
          }
          public String getDb() {
              return db;
          }
          public void setDb(String db) {
              this.db = db;
          }
      }
      
    • 修改DataSourceContextHolder,将DataSourceType改为String

      public class DataSourceContextHolder {
          // 存放当前线程使用的数据源类型
          private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
      
          // 设置数据源
          public static void setDataSource(String type){
              contextHolder.set(type);
          }
      
          // 获取数据源
          public static String getDataSource(){
              return contextHolder.get();
          }
      
          // 清除数据源
          public static void clearDataSource(){
              contextHolder.remove();
          }
      }
      
    • 修改MyBatisDataSourceConfig

      @Configuration
      @MapperScan(basePackages = "com.example.multidatabase2.mapper")
      public class MyBatisDataSourceConfig {
        ...
          @Bean
          public DynamicDataSource dataSource(@Qualifier("db1DataSource") DataSource db1DataSource,
                                              @Qualifier("db2DataSource") DataSource db2DataSource) {
              Map<Object, Object> map = new HashMap<>();
              // 添加的key为String类型
              map.put(DataSourceType.db1.getDb(), db1DataSource);
              map.put(DataSourceType.db2.getDb(), db2DataSource);
              // 通过单例获取对象
              DynamicDataSource dynamicDataSource = DynamicDataSource.getInstance();
              dynamicDataSource.setTargetDataSources(map);
              dynamicDataSource.setDefaultTargetDataSource(db1DataSource);
      
              return dynamicDataSource;
          }
          ...
      
    • 修改DataSourceAspect

      public class DataSourceAspect {
          @Before("@annotation(ds)")
          public void beforeDataSource(DataSource ds) {
              // 修改为String
              String value = ds.value().getDb();
              DataSourceContextHolder.setDataSource(value);
              log.info("当前使用的数据源为:{}", value);
          }
          ...
      }
      

    测试:

    @Service
    public class StudentService {
        //从db2中获取到drive-class、url、username、password信息
        @DataSource(DataSourceType.db2)
        public int test(String id, String username ){
                // 通过id获取到drive-class、url、username、password
                Map<String, Object> getdb = studentMapper.getdb(id);
            
                // 配置数据源
                HikariDataSource dataSource = new HikariDataSource();
                dataSource.setDriverClassName((String) getdb.get("class_name"));
                dataSource.setJdbcUrl((String)getdb.get("url"));
                dataSource.setUsername((String)getdb.get("username"));
                dataSource.setPassword((String)getdb.get("password"));
    
                // 添加一个数据源到多数据源中
                DynamicDataSource dynamicDataSource = DynamicDataSource.getInstance();
                Map<Object, Object> dataSourceMap = dynamicDataSource.getDataSourceMap();
                dataSourceMap.put(id, dataSource);
                dynamicDataSource.setTargetDataSources(dataSourceMap);
    
                // 切换数据源
                DataSourceContextHolder.setDataSource(id);
    
                // 获取用户信息
                Map<String, Object> map = studentMapper.selectStudent(1);
    
                // 更新id为1的用户信息
                int i = updateStudent2(username, 1);
    
                // 使用该数据源后,删除该数据源(如果不在使用)
                DynamicDataSource instance = DynamicDataSource.getInstance();
                Map<Object, Object> dataSourceMap = instance.getDataSourceMap();
                dataSourceMap.remove(id);
                instance.setTargetDataSources(dataSourceMap);
            
                return i;
            }
    
            public int updateStudent2(String username, int id){
                // 更新用户
                int i = studentMapper.updateStudent(username, id);
                return i;
            }
        }
    }
    

    上面就可以通过数据库获取信息进行配置数据源使用,使用后,可以删除。如果需要进行事务管理,可以把updateStudent2方法放在另一个类中,加上注解@Transactional(rollbackFor = Exception.class)即可。

    相关文章

      网友评论

          本文标题:SpringBoot2+Mybatis多数据源切换和动态增减

          本文链接:https://www.haomeiwen.com/subject/dlonzqtx.html