zoukankan      html  css  js  c++  java
  • 微服务架构【SpringBoot+SpringCloud+VUE】五 || 微信公众号-动态数据源

    1、源码分析

    在很多具体应用场景的时候,我们需要用到动态数据源的情况。比如读写分离、多租户场景等。本教程案例基于Spring Boot + Mybatis + MySQL实现。Spring内置了一个抽象类AbstractRoutingDataSource,它可以把多个数据源配置成一个Map,根据不同的key返回不同的数据源。因为AbstractRoutingDataSource也是一个DataSource接口,因此,应用程序可以先设置好key,访问数据库时就可以从AbstractRoutingDataSource拿到对应的一个真实的数据源,从而访问指定的数据库。接下来我们通过对源码进行分析,在项目中实现多数据源的切换:

    抽象类AbstractRoutingDataSource类中成员如下;

    private Map<Object, Object> targetDataSources;
    private Object defaultTargetDataSource;
    private Map<Object, DataSource> resolvedDataSources;
    private DataSource resolvedDefaultDataSource;
    
    • targetDataSources保存了key和数据库连接的映射关系
    • defaultTargetDataSource标识默认的连接
    • resolvedDataSources这个数据结构是通过targetDataSources构建而来,存储结构也是数据库标识和数据源的映射关系

    该类中的determineTargetDataSource()方法,通过调用连接数据库的getConnection()方法来创建连接。

    public Connection getConnection() throws SQLException {
        return this.determineTargetDataSource().getConnection();
    }
    public Connection getConnection(String username, String password) throws SQLException {
        return this.determineTargetDataSource().getConnection(username, password);
    }
    

    我们看一下determineTargetDataSource()方法,该方法决定spring容器连接那个数据源。其中,determineCurrentLookupKey()方法是抽象方法,需要我们继承AbstractRoutingDataSource抽象类来重写此方法。该方法返回一个key,并赋值给lookupKey。key值是bean中的beanName,由此key可以通过resolvedDataSources属性的键来获取对应的DataSource值,从而达到数据源切换的功能。

    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;
        }
    }
    

    我们继续看AbstractRoutingDataSource这个类,它实现了InitializingBean接口,并实现了afterPropertiesSet方法。afterPropertiesSet方法是初始化bean的时候执行,可以针对某个具体的bean进行执行。因为数据源bean是动态生成的,所有需要添加到targetDataSources中,然后调用afterPropertiesSet()方法,来通知spring有bean更新。所以在此方法中将targetDataSources属性的键值信息存储到resolvedDataSources属性中,以便后续调用。

    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);
            }
        }
    }
    

    2、项目中实现

    原理分析清楚后,那我们实现动态数据源切换就非常简单了,首先修改配置文件,添加两个数据源,根据实际情况进行配置,master和slave可以自定义,其余字段与单数据源一致。

    spring:
      datasource:
        master:
          driver-class-name: com.mysql.cj.jdbc.Driver
          username: root
          password: 123465
          jdbc-url: jdbc:mysql://localhost:3306/theme_weixin?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowMultiQueries=true&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
        slave:
          driver-class-name: com.mysql.cj.jdbc.Driver
          username: root
          password: 123465
          jdbc-url: jdbc:mysql://192.168.101.18:3306/theme_weixin?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowMultiQueries=true&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
    

    启动类添加exclude = {DataSourceAutoConfiguration.class},禁用数据源默认的自动配置。数据源默认自动配置会读取 spring.datasource.* 的属性创建数据源,所以要禁用以进行定制。

    @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
    public class WeixinApplication {
        public static void main(String[] args) {
            SpringApplication.run(WeixinApplication.class, args);
        }
    }
    

    创建一个数据源配置类,注入数据源配置属性,创建master、slave数据源

    @Configuration
    public class DataSourceConfig {
        @Primary
        @Bean
        @ConfigurationProperties(prefix = "spring.datasource.master")
        @RefreshScope
        public DataSource masterDataSource() {
            return DataSourceBuilder.create().build();
        }
        @Bean
        @ConfigurationProperties(prefix = "spring.datasource.slave")
        @RefreshScope
        public DataSource slaveDataSource() {
            return DataSourceBuilder.create().build();
        }
    }
    

    继承MybatisAutoConfiguration,将多数据源注入到SqlSessionFactory中

    @Configuration
    public class MyBatisConfig extends MybatisAutoConfiguration {
        @Resource(name = "masterDataSource")
        private DataSource masterDataSource;
        @Resource(name = "slaveDataSource")
        private DataSource slaveDataSource;
        public MyBatisConfig(MybatisProperties properties, ObjectProvider<Interceptor[]> interceptorsProvider, ObjectProvider<TypeHandler[]> typeHandlersProvider, ObjectProvider<LanguageDriver[]> languageDriversProvider, ResourceLoader resourceLoader, ObjectProvider<DatabaseIdProvider> databaseIdProvider, ObjectProvider<List<ConfigurationCustomizer>> configurationCustomizersProvider) {
            super(properties, interceptorsProvider, typeHandlersProvider, languageDriversProvider, resourceLoader, databaseIdProvider, configurationCustomizersProvider);
        }
        @Bean
        public SqlSessionFactory sqlSessionFactory() throws Exception {
            // 重载父类 sqlSessionFactory init
            return super.sqlSessionFactory(roundRobinDataSourceProxy());
        }
        private AbstractRoutingDataSource roundRobinDataSourceProxy() {
            DynamicDataSource proxy = new DynamicDataSource();
            Map<Object, Object> targetDataResources = new HashMap<>(2);
            targetDataResources.put(DbContextHolder.DbType.MASTER,masterDataSource);
            targetDataResources.put(DbContextHolder.DbType.SLAVE,slaveDataSource);
            proxy.setDefaultTargetDataSource(masterDataSource);
            proxy.setTargetDataSources(targetDataResources);
            proxy.afterPropertiesSet();
            return proxy;
        }
    }
    

    创建DynamicDataSource类,继承AbstractRoutingDataSource,重写determineCurrentLookupKey方法

    public class DynamicDataSource extends AbstractRoutingDataSource {
        @Override
        protected Object determineCurrentLookupKey() {
            return DbContextHolder.getDbType().key();
        }
    }
    

    DbContextHolder方法如下:

    public class DbContextHolder {
        public enum DbType{
            Master("masterDataSource"),
            Slave("slaveDataSource");
            String beanName;
            DbType(String beanName) {
                this.beanName = beanName;
            }
            public String key() {
                return this.beanName;
            }
        }
        private static final ThreadLocal<DbType> CONTEXT_HOLDER = new ThreadLocal<>();
        public static void setDbType(DbType dbType){
            if(dbType==null){
                throw new NullPointerException();
            }else{
                CONTEXT_HOLDER.set(dbType);
            }
        }
        public static DbType getDbType(){
            return CONTEXT_HOLDER.get()==null? DbType.MASTER:CONTEXT_HOLDER.get();
        }
        public static void clearDbType() {
            CONTEXT_HOLDER.remove();
        }
    }
    

    数据源可在方法中调用进行切换,或基于注解的方式切换。数据源切换示例:

    public String test() {
        DbContextHolder.setDbType(DbContextHolder.DbType.SLAVE);
        TabCategory tabCategory=new TabCategory();
        tabCategory.setCategoryId(10L);
        tabCategoryMapper.insertSelective(tabCategory);
        DbContextHolder.clearDbType();
        return "success";
    }
    

    3、学习交流QQ群【883210148】

    alt QQ群

    4、关注微信公众号,免费获取文档及资源

    alt 微信公众号

  • 相关阅读:
    移动端布局方案汇总&&原理解析
    Javascript运行机制
    git 使用
    async await详解
    vue使用axios调用豆瓣API跨域问题
    hash和history的区别
    http状态码
    XSS 和 CSRF简述及预防措施
    【pytorch】pytorch基础学习
    [源码解读] ResNet源码解读(pytorch)
  • 原文地址:https://www.cnblogs.com/kevin-ying/p/12392721.html
Copyright © 2011-2022 走看看