1.1 简述

最近项目中有动态切换数据源需求,主要是要动态切换数据源进行数据的获取,现将项目中实现思路及代码提供出来,供大家参考。当然切换数据源还有其他的方式比如使用切面的方式,其实大体思路是一样的。

1.2 设计思路与代码示例

数据库连接池druid

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

ymldruid配置:

druid:initial-size: 5                                       # 初始化大小min-idle: 10                                          # 最小连接数max-active: 20                                        # 最大连接数max-wait: 60000                                       # 获取连接时的最大等待时间

配置文件中配置默认的数据源datasource,默认数据源中需要建立一张各租户的数据源表:

上图表中有redis-database字段为切换redis库时使用的字段,如不需要可自行修改。

创建DataSourceContextHolder用于切换数据源,使用threadlocal保留当前线程数据源信息:

@Slf4j
public class DataSourceContextHolder {private static final ThreadLocal<String> DATA_SOURCE = new ThreadLocal<>();/*** 切换数据源*/public static void setDataSource(String datasourceId) {DATA_SOURCE.set(datasourceId);log.info("已切换到数据源:{}",datasourceId);}public static String getDataSource() {return DATA_SOURCE.get();}/*** 删除数据源*/public static void removeDataSource() {DATA_SOURCE.remove();log.info("已切换到默认主数据源");}
}

继承AbstractRoutingDataSource实现根据不同请求切换数据源:

@Slf4j
@Data
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {private boolean debug = true;/*** 存储注册的数据源*/private volatile Map<Object, Object> custom;@Overrideprotected Object determineCurrentLookupKey() {String datasourceId = DataSourceContextHolder.getDataSource();if(!StringUtils.isEmpty(datasourceId)){Map<Object, Object> map = this.custom;if(map.containsKey(datasourceId)){log.info("当前数据源是:{}",datasourceId);}else{log.info("不存在数据源:{}",datasourceId);return null;}}else{log.info("当前是默认数据源");}return datasourceId;}@Overridepublic void setTargetDataSources(Map<Object, Object> param) {super.setTargetDataSources(param);this.custom = param;}/*** @Description: 检查数据源是否已经创建* @param dataSource*/public void checkCreateDataSource(DatabaseList dataSource){String datasourceId = dataSource.getFactoryCode();Map<Object, Object> map = this.custom;if(map.containsKey(datasourceId)){//这里检查一下之前创建的数据源,现在是否连接正常DruidDataSource druidDataSource = (DruidDataSource) map.get(datasourceId);boolean flag = true;DruidPooledConnection connection = null;try {connection = druidDataSource.getConnection();} catch (SQLException throwAbles) {//抛异常了说明连接失效吗,则删除现有连接log.error(throwAbles.getMessage());flag = false;delDataSources(datasourceId);//}finally {//如果连接正常记得关闭if(null != connection){try {connection.close();} catch (SQLException e) {log.error(e.getMessage());}}}if(!flag){createDataSource(dataSource);}}else {createDataSource(dataSource);}}/*** @Description: 创建数据源* @param dataSource*/private void createDataSource(DatabaseList dataSource) {try {Class.forName("com.mysql.cj.jdbc.Driver");Connection connection = DriverManager.getConnection(dataSource.getUrl(), dataSource.getUser(), dataSource.getPassword());if(connection==null){log.error("数据源配置有错误,DataSource:{}",dataSource);}else{connection.close();}DruidDataSource druidDataSource = new DruidDataSource();druidDataSource.setName(dataSource.getFactoryCode());druidDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");druidDataSource.setUrl(dataSource.getUrl());druidDataSource.setUsername(dataSource.getUser());druidDataSource.setPassword(dataSource.getPassword());druidDataSource.setMaxActive(20);druidDataSource.setMinIdle(5);//获取连接最大等待时间,单位毫秒druidDataSource.setMaxWait(6000);String validationQuery = "select 1 from dual";//申请连接时执行validationQuery检测连接是否有效,防止取到的连接不可用druidDataSource.setTestOnBorrow(true);druidDataSource.setValidationQuery(validationQuery);druidDataSource.init();this.custom.put(dataSource.getFactoryCode(),druidDataSource);// 将map赋值给父类的TargetDataSourcessetTargetDataSources(this.custom);// 将TargetDataSources中的连接信息放入resolvedDataSources管理super.afterPropertiesSet();} catch (Exception e) {log.error("数据源创建失败",e);}}/*** @Description: 删除数据源* @param datasourceId*/private void delDataSources(String datasourceId) {Map<Object, Object> map = this.custom;Set<DruidDataSource> druidDataSourceInstances = DruidDataSourceStatManager.getDruidDataSourceInstances();for (DruidDataSource dataSource : druidDataSourceInstances) {if (datasourceId.equals(dataSource.getName())) {map.remove(datasourceId);//从实例中移除当前dataSourceDruidDataSourceStatManager.removeDataSource(dataSource);// 将map赋值给父类的TargetDataSourcessetTargetDataSources(map);// 将TargetDataSources中的连接信息放入resolvedDataSources管理super.afterPropertiesSet();}}}
}

接下来配置默认数据源,还有数据库连接池的信息,以及将数据源配置到sql工厂当中:

@Configuration
@Slf4j
public class DruidDBConfig {@Value("${spring.datasource.url}")private String dbUrl;@Value("${spring.datasource.username}")private String username;@Value("${spring.datasource.password}")private String password;@Value("${spring.datasource.driver-class-name}")private String driverClassName;@Value("${spring.datasource.druid.initial-size}")private int initialSize;@Value("${spring.datasource.druid.min-idle}")private int minIdle;@Value("${spring.datasource.druid.max-active}")private int maxActive;@Value("${spring.datasource.druid.max-wait}")private int maxWait;@Bean@Primary@Qualifier("mainDataSource")public DataSource dataSource() throws SQLException {DruidDataSource datasource = new DruidDataSource();// 基础连接信息datasource.setUrl(this.dbUrl);datasource.setUsername(username);datasource.setPassword(password);datasource.setDriverClassName(driverClassName);// 连接池连接信息datasource.setInitialSize(initialSize);datasource.setMinIdle(minIdle);datasource.setMaxActive(maxActive);datasource.setMaxWait(maxWait);//是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。datasource.setPoolPreparedStatements(false);datasource.setMaxPoolPreparedStatementPerConnectionSize(20);//申请连接时执行validationQuery检测连接是否有效,这里建议配置为TRUE,防止取到的连接不可用datasource.setTestOnBorrow(true);//建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。datasource.setTestWhileIdle(true);//用来检测连接是否有效的sqldatasource.setValidationQuery("select 1 from dual");//配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒datasource.setTimeBetweenEvictionRunsMillis(60000);//配置一个连接在池中最小生存的时间,单位是毫秒,这里配置为3分钟180000datasource.setMinEvictableIdleTimeMillis(180000);datasource.setKeepAlive(true);return datasource;}@Bean(name = "dynamicDataSource")@Qualifier("dynamicDataSource")public DynamicRoutingDataSource dynamicDataSource() throws SQLException {DynamicRoutingDataSource dynamicDataSource = new DynamicRoutingDataSource();dynamicDataSource.setDebug(false);//配置缺省的数据源dynamicDataSource.setDefaultTargetDataSource(dataSource());Map<Object, Object> targetDataSources = new HashMap<Object, Object>();//额外数据源配置 TargetDataSourcestargetDataSources.put("mainDataSource", dataSource());dynamicDataSource.setTargetDataSources(targetDataSources);return dynamicDataSource;}@Beanpublic SqlSessionFactory sqlSessionFactory() throws Exception {MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();sqlSessionFactoryBean.setDataSource(dynamicDataSource());//对新的SqlSessionFactory配置 修改mybatis-plus Page自动分页失效问题 以及 找不到xml问题MybatisConfiguration configuration = new MybatisConfiguration();configuration.addInterceptor(new MybatisPlusConfig().paginationInterceptor());sqlSessionFactoryBean.setConfiguration(configuration);sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:/mapper/*.xml"));return sqlSessionFactoryBean.getObject();}/*** @Description: 将动态数据加载类添加到事务管理器* @param dataSource* @return org.springframework.jdbc.datasource.DataSourceTransactionManager*/@Beanpublic DataSourceTransactionManager transactionManager(DynamicRoutingDataSource dataSource) {return new DataSourceTransactionManager(dataSource);}
}

现在就可以根据数据源表中唯一值切换数据源了,调用changeDB(数据源唯一值):

@ResourceDatabaseListService databaseListService;@Resourceprivate DynamicRoutingDataSource dynamicRoutingDataSource;@Overridepublic boolean changeDB(String datasourceId) {//切到默认数据源DataSourceContextHolder.removeDataSource();//找到所有的配置List<DatabaseList> databaseListList = databaseListService.list();if(!CollectionUtils.isEmpty(databaseListList)){for (DatabaseList d : databaseListList) {if(d.getFactoryCode().equals(datasourceId)){//判断连接是否存在,不存在就创建dynamicRoutingDataSource.checkCreateDataSource(d);//切换数据源DataSourceContextHolder.setDataSource(d.getFactoryCode());return true;}}}return false;}

以上就完成了所有步骤,可以切换数据源了。

在我的项目里,我在登录的拦截器中校验完权限后判断属于哪个数据源来切换。

下面说一下redis库的动态切换:

同样使用threadlocal保留当前线程的redis库,在getset时设置redis数据库索引:

@Slf4j
@Component
public class RedisUtil {private static RedisTemplate redisTemplate;@Autowiredpublic void setRedisTemplate(RedisTemplate redisTemplate) {RedisUtil.redisTemplate = redisTemplate;}private static ThreadLocal<Integer>   database          = new ThreadLocal<Integer>();public static Integer getDatabase() {return (Integer) database.get();}public static void setDatabase(Integer db) {database.set(db);}/*** 设置数据库索引**/public static void setDbIndex() {//默认使用1Integer dbIndex = 1;if(database.get()!=null){dbIndex = database.get();}LettuceConnectionFactory redisConnectionFactory = (LettuceConnectionFactory) redisTemplate.getConnectionFactory();if (redisConnectionFactory == null) {return;}redisConnectionFactory.setDatabase(dbIndex);redisTemplate.setConnectionFactory(redisConnectionFactory);// 属性设置后redisConnectionFactory.afterPropertiesSet();// 重置连接redisConnectionFactory.resetConnection();}/*** 普通缓存获取** @param key 键* @return 值*/public static Object get(String key) {setDbIndex();return key == null ? null : redisTemplate.opsForValue().get(key);}/*** 普通缓存放入** @param key   键* @param value 值* @return true成功 false失败*/public static boolean set(String key, Object value) {setDbIndex();try {redisTemplate.opsForValue().set(key, value);return true;} catch (Exception e) {e.printStackTrace();return false;}}

1.3 原理解析

原理参考:https://blog.csdn.net/zhang_java_11/article/details/121626842?spm=1001.2014.3001.5506

其实我们新建数据库连接的时候也是通过DataSource来获取连接的,这里的AbstractRoutingDataSource也是通过了DataSource中的getConnection方法来获取连接的。

这个类里维护了两个Map来存储数据库连接信息:

@Nullable
private Map<Object, Object> targetDataSources; @Nullable
private Object defaultTargetDataSource;private boolean lenientFallback = true;private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();@Nullable
private Map<Object, DataSource> resolvedDataSources;@Nullable
private DataSource resolvedDefaultDataSource;

下面对上面的几个属性进行说明:

  • 其中第一个targetDataSources是一个Map对象,在我们上面第五步创建DynamicDataSource实例的时候将多个数据源的DataSource类,放入到这个Map中去,这里的Key是枚举类,values就是DataSource类。
  • 第二个defaultTargetDataSource是默认的数据源,就是DynamicDataSource中唯一重写的方法来给这个对象赋值的。
  • 第三个lenientFallback是一个标识,是当指定数据源不存在的时候是否采用默认数据源,默认是true,设置为false之后如果找不到指定数据源将会返回null.
  • 第四个dataSourceLookup是用来解析指定的数据源对象为DataSource实例的。默认是JndiDataSourceLookup实例,继承自DataSourceLookup接口。
  • 第五个resolvedDataSources也是一个Map对象,这里是存放指定数据源解析后的DataSource对象。

    第六个resolvedDefaultDataSource是默认的解析后的DataSource数据源对象上面的getConnection方法就是从这个变量中拿到DataSource实例并获取连接的。

1.4 总结

多数据源切换的需求在我们日常开发中可能会经常碰到,主要是通过几个主要的类进行实现的,实现过程并不复杂,在此记录一下,希望能够帮助到有需要的童鞋。

Springboot/MybatisPlus动态切换数据源相关推荐

  1. springboot 中动态切换数据源(多数据源应用设计)

    目录 原理图 数据库 项目结构 启动类 entity controller service mapper 配置文件 线程上下文 (DataSourceHolder) 动态数据源 DynamicData ...

  2. SpringBoot+AOP实现动态切换数据源

    首先描述下笔者的基本步骤: 1.yml配置文件中定义多数据源: 2.自定义一个注解类(@DataSource 可标注在类或方法上),定义String类型的变量value,value中存储数据源名称: ...

  3. Spring Boot多数据源配置并通过注解实现动态切换数据源

    文章目录 1. AbstractRoutingDataSource类介绍 2. ThreadLocal类介绍 3. 环境准备 3.1 数据库准备 3.2 项目创建 4. 具体实现 4.1 定义数据源枚 ...

  4. SpringBoot MybatisPlus Druid 多数据源项目

    写在前面:本文主要介绍SpringBoot MybatisPlus和Druid这些组件下,如何创建多数据源(DataSource)的web项目.写这篇博客的原因就是我在搜索同类型的问题的时候,其他人写 ...

  5. 动态切换数据源(spring+hibernate)

    起因:在当前我手上的一个项目中需要多个数据源,并且来自于不同类型的数据库... 因为很多历史原因.这个项目的住数据源是MySQL,整个系统的CURD都是操作的这个数据库. 但是还有另外两个用于数据采集 ...

  6. mybatis手动切换数据库_在Spring项目中使用 Mybatis 如何实现动态切换数据源

    在Spring项目中使用 Mybatis 如何实现动态切换数据源 发布时间:2020-11-17 16:20:11 来源:亿速云 阅读:108 作者:Leah 这篇文章将为大家详细讲解有关在Sprin ...

  7. Spring学习总结(16)——Spring AOP实现执行数据库操作前根据业务来动态切换数据源

    深刻讨论为什么要读写分离? 为了服务器承载更多的用户?提升了网站的响应速度?分摊数据库服务器的压力?就是为了双机热备又不想浪费备份服务器?上面这些回答,我认为都不是错误的,但也都不是完全正确的.「读写 ...

  8. Java实现动态切换数据源

    在一般的Java项目中,如果使用Spring去管理数据库连接信息,一般只能连接一个数据库,可是会有部分情况我们需要连接多个数据库,甚至还会存在不同的请求需要根据配置信息连接不同的数据库,比如: 在很多 ...

  9. 通过切面动态切换数据源

    标题业务场景:大量的项目的数据库需要从db2迁移到oracle,为项目上线后出现不可预料的错误可及时回退,需要可以随时切换数据源 以控制层为切点,动态切换数据源,通过查询数据库查询当前应用的开关,来判 ...

最新文章

  1. 利用Keras构建自动编码器
  2. 学习linux之用户-文件-权限操作
  3. bash下个人习惯的一些文件设置
  4. [LeetCode]Search Insert Position
  5. java netty和dubbo_Dubbo与Netty杂谈
  6. XML读取信息并显示
  7. mongoose c++封装
  8. “去QE”时代下,QE如何破茧重生?
  9. [剑指offer] 7. 斐波那契数列 (递归 时间复杂度)
  10. printf 输出格式、域宽
  11. 1 常用邮箱SMTP/POP3地址及端口
  12. 钓鱼网站制作 ---- Setoolkit 克隆web页面钓鱼
  13. 11408考研复习规划
  14. 【记录】非常实用,Python编码规范的一些建议(1)
  15. 工作环境的改善---提高工作效率和工作质量
  16. 假币问题python
  17. react插槽Protal
  18. 如何让学习像打游戏一样具有成瘾性
  19. 【随笔记】Deepin20系统更换fish,替代bash
  20. 计算机专业试讲10分钟教案,10分钟试讲教案模板.doc

热门文章

  1. 《淘宝店铺设计装修一册通》一2.5 抠图工具的简单运用
  2. div css圆环布局,CSS圆环样式
  3. 四元数与矩阵欧拉角之间的相互转换
  4. Android逆向-Android基础逆向7(内购干货集合)
  5. 华为交换机错包问题(inerrors)
  6. 微软要给Windows指定默认的终端了?它很酷
  7. 抖音矩阵号/抖音短视频SEO矩阵系统源码开发,优化排名。
  8. html4播放mp3,在网页播放MP3、WMA音乐的代码
  9. 基于web的视频_如何创建基于Web的视频播放器
  10. Java获取时间戳方法比较