SpringBoot动态多数据源实现方式

SpringBoot动态多数据源实现方式

一、概述

有时在项目中可能会使用到多个数据源,并且生产与开发、测试环境所需配置的个数均不一样,这种场景下就需要使用动态多数据源的方式去解决。

二、实现

本文将介绍如何通过Springboot的application.properties的配置+代码,实现通过注解或者动态数据源bean动态切换多数据源。

话不多说直接贴代码。

application.properties配置

# 主数据源
spring.datasource.db-type = com.alibaba.druid.pool.DruidDataSource
spring.datasource.druid.driverClassName = com.mysql.jdbc.Driver
spring.datasource.druid.url = jdbc:mysql://127.0.0.1/master?useUnicode=true&characterEncoding=utf8&useSSL=false&zeroDateTimeBehavior=convertToNull
spring.datasource.druid.username = root
spring.datasource.druid.password = 
spring.datasource.druid.locations = classpath:/mapper/*.xml

# 动态数据源1 在项目中所需切换进行业务处理的特殊数据源1
platform.data-source-model-config.ali.url = jdbc:mysql://127.0.0.1:3306/dynamic1?useUnicode=true&characterEncoding=utf8&useSSL=false&zeroDateTimeBehavior=convertToNull
platform.data-source-model-config.ali.username = root
platform.data-source-model-config.ali.password = 
platform.data-source-model-config.ali.driver-class-name = com.mysql.jdbc.Driver
# 动态数据源2 在项目中所需切换进行业务处理的特殊数据源2
platform.data-source-model-config.hw.url = jdbc:mysql://127.0.0.1:3306/dynamic2?useUnicode=true&characterEncoding=utf8&useSSL=false&zeroDateTimeBehavior=convertToNull
platform.data-source-model-config.hw.username = root
platform.data-source-model-config.hw.password = 
platform.data-source-model-config.hw.driver-class-name = com.mysql.jdbc.Driver

多数据源信息读取配置类

/**
 * 多数据源配置信息
 */
@Data
@Configuration
@ConfigurationProperties(prefix = "platform")
public class DataSourceModelConfig {
    public Map<String, DataSourceModel> dataSourceModelConfig;

    @Data
    public static class DataSourceModel {
        private String url;
        private String username;
        private String password;
        private String driverClassName;
    }


    /**
     * 获取该平台是否有配置数据源
     * @param dbTypeEnum
     * @return
     */
    public boolean getEnabled(DBTypeEnum dbTypeEnum) {
        return !ObjectUtil.isNull(this.dataSourceModelConfig.get(dbTypeEnum.getValue()));
    }
}
public enum DBTypeEnum {

    /**
     * 主数据源
     */
    master("master"),

    hw("hw"),
    /**
     * 动态数据源
     */
    ali("ali");


    private String value;

    DBTypeEnum(String value) {
        this.value = value;
    }

    public String getValue() {
        return value;
    }

}
DataSourceConfigurer.class修改德鲁伊动态数据源相关配置

@Configuration
@Slf4j
@EnableConfigurationProperties({DruidDataSourceProperties.class})
public class DataSourceConfigurer {

    @Autowired
    private DruidConfigure druidConfigure;

    /**
     * 动态数据源配置
     *
     * @return
     */
    @Bean
    @Primary
    public DataSource dynamicDataSource() {
        DynamicDataSource dynamicDataSource = DynamicDataSource.getInstance();
        //自定义数据源key值,将创建好的数据源对象,赋值到targetDataSources中,用于切换数据源时指定对应key即可切换
        DruidDataSource defaultDruidDataSource = druidConfigure.druidDataSource();

        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DBTypeEnum.operation.getValue(), defaultDruidDataSource);
        dynamicDataSource.setTargetDataSources(targetDataSources);
        dynamicDataSource.setDefaultTargetDataSource(defaultDruidDataSource);
        return dynamicDataSource;
    }

    /**
     *  配置mybatis的sqlSession连接动态数据源
     *
     * @param dynamicDataSource
     * @return
     * @throws Exception
     */
    @Bean
    public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource) throws Exception {
        MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dynamicDataSource);
//        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:/mapper/*Mapper.xml"));
        sqlSessionFactoryBean.setMapperLocations(resolveMapperLocations("classpath:/mapper/*.xml"));
        sqlSessionFactoryBean.setTransactionFactory(new JdbcTransactionFactory());
        //mybatis配置
        MybatisConfiguration mybatisConfiguration = new MybatisConfiguration();
        mybatisConfiguration.setDefaultScriptingLanguage(MybatisXMLLanguageDriver.class);
        mybatisConfiguration.setJdbcTypeForNull(JdbcType.NULL);
        mybatisConfiguration.setCallSettersOnNulls(true);
        mybatisConfiguration.setLogImpl(MybatisCustomLogConfigure.class);

        sqlSessionFactoryBean.setConfiguration(mybatisConfiguration);
        sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
        GlobalConfig config = new GlobalConfig();
        GlobalConfig.DbConfig dbConfig = new GlobalConfig.DbConfig();
        dbConfig.setSelectStrategy(FieldStrategy.NOT_EMPTY);
        dbConfig.setInsertStrategy(FieldStrategy.NOT_EMPTY);
        dbConfig.setUpdateStrategy(FieldStrategy.NOT_EMPTY);
        config.setDbConfig(dbConfig);
        //去掉banner
        config.setBanner(false);
        sqlSessionFactoryBean.setGlobalConfig(config);
        return sqlSessionFactoryBean.getObject();
    }

    @Bean
    public PlatformTransactionManager transactionManager() {
        return new DataSourceTransactionManager(dynamicDataSource());
    }

    private Resource[] resolveMapperLocations(String locations) {
        ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
        List<String> mapperLocations = Arrays.asList(locations.split(";"));
        List<Resource> resources = new ArrayList<>();
        if (!CollectionUtils.isEmpty(mapperLocations)) {
            for (String mapperLocation : mapperLocations) {
                Resource[] mappers = null;
                try {
                    mappers = resourceResolver.getResources(mapperLocation);
                } catch (IOException e) {
                    log.warn("mybatis locations load fail...");
                    continue;
                }
                resources.addAll(Arrays.asList(mappers));
            }
        }
        return resources.toArray(new Resource[resources.size()]);
    }





    public DruidDataSource createDataSource(String dbUrl, String userName, String password) {

        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource.setUrl(dbUrl);
        dataSource.setUsername(userName);
        dataSource.setPassword(password);
        // 初始化大小,最小,最大
        dataSource.setInitialSize(1);
        dataSource.setMinIdle(1);
        dataSource.setMaxActive(20);
        // 配置获取连接等待超时的时间
        dataSource.setMaxWait(60000);
        // 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
        dataSource.setTimeBetweenEvictionRunsMillis(60000);
        // 配置一个连接在池中最小生存的时间,单位是毫秒
        dataSource.setMinEvictableIdleTimeMillis(300000);
        dataSource.setValidationQuery("SELECT 1 FROM DUAL");
        dataSource.setTestWhileIdle(true);
        dataSource.setTestOnBorrow(false);
        dataSource.setTestOnReturn(false);
        // 打开PSCache,并且指定每个连接上PSCache的大小
        dataSource.setPoolPreparedStatements(true);
        dataSource.setMaxPoolPreparedStatementPerConnectionSize(20);
        //排查问题,之后代码要删除,影响性能
//        dataSource.setRemoveAbandoned(true);
//        dataSource.setRemoveAbandonedTimeout(180);
//        dataSource.setLogAbandoned(true);
        // 通过connectProperties属性来打开mergeSql功能;慢SQL记录
        Properties properties = new Properties();
        properties.setProperty("druid.stat.mergeSql", "true");
        properties.setProperty("druid.stat.slowSqlMillis", "5000");
        dataSource.setConnectProperties(properties);
        try {
            List<Filter> proxyFilters = new ArrayList<>();
            WallFilter wallFilter = new WallFilter();
            WallConfig config = new WallConfig();
            // 批量操作
            config.setMultiStatementAllow(true);
            config.setNoneBaseStatementAllow(true);
            config.setCommentAllow(true);
            wallFilter.setConfig(config);
            proxyFilters.add(wallFilter);
            dataSource.setFilters("stat,config");
            dataSource.setProxyFilters(proxyFilters);
            dataSource.init();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return dataSource;

    }
#DynamicDataSource.class 通过继承AbstractRoutingDataSource重写determineCurrentLookupKey()方法实现多数据源动态切换
public class DynamicDataSource extends AbstractRoutingDataSource {

    private final Logger logger = LoggerFactory.getLogger(DynamicDataSource.class);

    private static DynamicDataSource instance;

    // 用于存放已实例的数据源map
    private static Map<Object, Object> dataSourceMap = new ConcurrentHashMap<>();

    /**
     * 获取当前数据源
     *
     * @return
     */
    @Override
    protected Object determineCurrentLookupKey() {
        logger.debug("current DataSource is [{}]", DbContextHolder.getDbType());
        return DbContextHolder.getDbType();
    }

    /**
     * 设置数据源
     *
     * @param targetDataSources
     */
    @Override
    public void setTargetDataSources(Map<Object, Object> targetDataSources) {
        dataSourceMap.putAll(targetDataSources);
        super.setTargetDataSources(dataSourceMap);
        // 必须添加该句,否则新添加数据源无法识别到
        super.afterPropertiesSet();
    }


    /**
     * 获取存储已实例的数据源map
     *
     * @return
     */
    public Map<Object, DataSource> getDataSourceMap() {
        return super.getResolvedDataSources();
    }

    /**
     * 是否存在当前key的 DataSource
     *
     * @param key
     * @return 存在返回 true, 不存在返回 false
     */
    public static boolean isExistDataSource(String key) {
        return dataSourceMap.containsKey(key);
    }

    /**
     * 单例方法
     *
     * @return
     */
    public static synchronized DynamicDataSource getInstance() {
        if (instance == null) {
            synchronized (DynamicDataSource.class) {
                if (instance == null) {
                    instance = new DynamicDataSource();
                }
            }
        }
        return instance;
    }
    
    


DbContextHolder.class 数据源信息上下文

public class DbContextHolder {

    private static final ThreadLocal<String> contextHolder = new InheritableThreadLocal<String>() {
        @Override
        protected String initialValue() {
            return DBTypeEnum.operation.getValue();
        }
    };

    /**
     * 设置数据源
     *
     * @param db
     */
    public static void setDbType(String db) {
        contextHolder.set(db);
    }

    /**
     * 取得当前数据源
     *
     * @return
     */
    public static String getDbType() {
        return (String) contextHolder.get();
    }

    /**
     * 清除上下文数据
     */
    public static void clearDbType() {
        contextHolder.remove();
    }


}
#动态数据源注解类
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.TYPE })
public @interface DataSourceSwitch {

    DBTypeEnum value() default DBTypeEnum.operation;
}
#动态数据源相关切面
@Aspect
public class DataSourceSwitchAspect {

    private static final Logger log = LoggerFactory.getLogger(DataSourceSwitchAspect.class);

    @Autowired
    private SwitchDB switchDB;

    @Pointcut("@annotation(com.test.master.config.DataSourceSwitch)")
    public void dbPointcut() {
    }


    @Before("dbPointcut()")
    public void doBefore(JoinPoint joinPoint) {
        MethodSignature method = (MethodSignature) joinPoint.getSignature();
        DataSourceSwitch dataSourceSwitch = method.getMethod().getAnnotation(DataSourceSwitch.class);
        switchDB.change(dataSourceSwitch.value());

    }

    @After("dbPointcut()")
    public void doAfter(JoinPoint joinPoint) {
        DbContextHolder.clearDbType();
    }

}
#动态数据源bean,可调用change方法进行切换数据源
@Component("switchDB")
public class SwitchDB {

    private final Logger logger = LoggerFactory.getLogger(SwitchDB.class);

    @Autowired
    private DataSourceConfigurer dataSourceConfigurer;
    @Autowired
    private DataSourceModelConfig dataSourceModelConfig;


    /**
     * 切换数据源
     *
     * @return
     */
    public void change(DBTypeEnum dbType) {
        //获取当前连接的数据源对象的key
        String currentKey = DbContextHolder.getDbType();
        //如果当前数据库连接已经是想要的连接,则直接返回
        if (!currentKey.equals(dbType.getValue())) {
            //判断储存动态数据源实例的map中key值是否存在
            if (DynamicDataSource.isExistDataSource(dbType.getValue())) {
                DbContextHolder.setDbType(dbType.getValue());
                logger.info("库名:[{}], 切换完毕",dbType.getValue());
            } else {
                DbContextHolder.setDbType(dbType.getValue());
                logger.info("数据源key:{} 不存在,开始创建对应数据源", dbType.getValue());
                initDataSource();
                DbContextHolder.setDbType(dbType.getValue());
            }
        }
    }

    /**
     * 新增租户库
     *
     */
    public void initDataSource() {
        Map<Object, Object> result = new HashMap<>();
        dataSourceModelConfig.dataSourceModelConfig.forEach((key, dataSourceModel) -> {
            String testUrl = dataSourceModel.getUrl() + "&user=" + dataSourceModel.getUsername() + "&password=" + dataSourceModel.getPassword();
            if (dataSourceConfigurer.jdbcTest(testUrl)) {
                DruidDataSource druidDataSource = dataSourceConfigurer.createDataSource(dataSourceModel.getUrl(), dataSourceModel.getUsername(), dataSourceModel.getPassword());
                result.put(key, druidDataSource);
            }
        });
        DynamicDataSource.getInstance().setTargetDataSources(result);

    }

    /**
     * 清除当前线程数据源
     */
    public void clearCurrentDataSource() {
        DbContextHolder.clearDbType();
    }

相关测试类代码

@RunWith(SpringRunner.class)
@SpringBootTest(classes = TestApplication.class)
public class JobTest {

    @Resource
    private SwitchDB switchdb;

    @Test
    public void test1() {
        switchDB.change(DBTypeEnum.master);
        System.out.println("切换至master数据源");
    }

    @Test
    @DataSourceSwitch(value = DBTypeEnum.master) #通过注解方式将该方法切换至指定数据源
    public void test2() {
        System.out.println("切换至master数据源");
    }


}

三、实现原理

org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource实现了DataSource接口,在调用unwrap()方法时会调用AbstractRoutingDataSource的determineTargetDataSource()方法,从中获取数据源。而determineTargetDataSource()方法将调用determineCurrentLookupKey()方法来动态获取数据源,因此我们需要做的就是编写子类继承AbstractRoutingDataSource,重写其**determineCurrentLookupKey()**方法获取我们需要切换的数据源即可。

在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Spring Boot是一种现代化的Java开发框架,通过使用它,我们可以更加快速和方便地构建出高效、高质量的应用程序。 在使用Spring Boot的过程中,我们经常会面临多数据源的需求。Spring Boot针对这种场景提供了一些解决方案,其中较为流行的是使用Spring Boot多数据源特性。 Spring Boot多数据源特性分为静态多数据源动态多数据源两种,其中动态多数据源相比较而言更加灵活。动态多数据源实现原理是:在应用程序启动之前,我们需要将所有的数据源信息存储在内存中,然后通过程序动态地将这些数据源注入到DataSource对象中去。 在Spring Boot中,我们可以使用AbstractRoutingDataSource类来实现动态多数据源。该类实现javax.sql.DataSource接口,并提供了getDataSource()方法。在该方法中,我们可以根据不同的数据源标识符,返回对应的实际数据源对象。 为了使用AbstractRoutingDataSource类,我们需要自定义一个数据源路由类,继承AbstractRoutingDataSource类,实现determineCurrentLookupKey()方法。该方法用来决定当前线程所需要使用的数据源标识符,通常可以从ThreadLocal中获取。 最后,我们需要在Spring Boot的配置文件中添加多个数据源配置,并将自定义的数据源路由类作为默认数据源。 总之,Spring Boot提供了非常方便的动态多数据源特性,通过使用它,我们可以更加灵活地处理多数据源的需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值