如何通过Spring Boot配置动态数据源访问多个数据库
之前写过一篇博客《Spring+Mybatis+Mysql搭建分布式数据库访问框架》描述如何通过Spring+Mybatis配置动态数据源访问多个数据库。但是之前的方案有一些限制(原博客中也描述了):只适用于数据库数量不多且固定的情况。针对数据库动态增加的情况无能为力。
下面讲的方案能支持数据库动态增删,数量不限。
数据库环境准备
下面以Mysql为例,先在本地建3个数据库用于测试。需要说明的是本方案不限数据库数量,支持不同的数据库部署在不同的服务器上。如图所示db_project_001、db_project_002、db_project_003。
搭建Java后台微服务项目
创建一个Spring Boot的maven项目:
config:数据源配置。
datasource:自己实现的动态数据源相关类。
dbmgr:管理项目编码与数据库IP、名称的映射关系(实际项目中这部分数据保存在redis缓存中,可动态增删)。
mapper:mybatis的数据库访问接口。
model:映射模型。
rest:微服务对外发布的restful接口,这里用来测试。
application.yml:配置数据库JDBC参数。
详细的代码实现
1. 数据源配置管理类(DataSourceConfig.java)
package com.elon.dds.config; import javax.sql.DataSource; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.mybatis.spring.annotation.MapperScan; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import com.elon.dds.datasource.DynamicDataSource; /** * 数据源配置管理。 * * @author elon * @version 2018年2月26日 */ @Configuration @MapperScan(basePackages="com.elon.dds.mapper", value="sqlSessionFactory") public class DataSourceConfig { /** * 根据配置参数创建数据源。使用派生的子类。 * * @return 数据源 */ @Bean(name="dataSource") @ConfigurationProperties(prefix="spring.datasource") public DataSource getDataSource() { DataSourceBuilder builder = DataSourceBuilder.create(); builder.type(DynamicDataSource.class); return builder.build(); } /** * 创建会话工厂。 * * @param dataSource 数据源 * @return 会话工厂 */ @Bean(name="sqlSessionFactory") public SqlSessionFactory getSqlSessionFactory(@Qualifier("dataSource") DataSource dataSource) { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); try { return bean.getObject(); } catch (Exception e) { e.printStackTrace(); return null; } } }
2. 定义动态数据源
1) 首先增加一个数据库标识类,用于区分不同的数据库(DBIdentifier.java)
由于我们为不同的project创建了单独的数据库,所以使用项目编码作为数据库的索引。而微服务支持多线程并发的,采用线程变量。
package com.elon.dds.datasource; /** * 数据库标识管理类。用于区分数据源连接的不同数据库。 * * @author elon * @version 2018-02-25 */ public class DBIdentifier { /** * 用不同的工程编码来区分数据库 */ private static ThreadLocal<String> projectCode = new ThreadLocal<String>(); public static String getProjectCode() { return projectCode.get(); } public static void setProjectCode(String code) { projectCode.set(code); } }
2) 从DataSource派生了一个DynamicDataSource,在其中实现数据库连接的动态切换(DynamicDataSource.java)
package com.elon.dds.datasource; import java.lang.reflect.Field; import java.sql.Connection; import java.sql.SQLException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.tomcat.jdbc.pool.DataSource; import org.apache.tomcat.jdbc.pool.PoolProperties; import com.elon.dds.dbmgr.ProjectDBMgr; /** * 定义动态数据源派生类。从基础的DataSource派生,动态性自己实现。 * * @author elon * @version 2018-02-25 */ public class DynamicDataSource extends DataSource { private static Logger log = LogManager.getLogger(DynamicDataSource.class); /** * 改写本方法是为了在请求不同工程的数据时去连接不同的数据库。 */ @Override public Connection getConnection(){ String projectCode = DBIdentifier.getProjectCode(); //1、获取数据源 DataSource dds = DDSHolder.instance().getDDS(projectCode); //2、如果数据源不存在则创建 if (dds == null) { try { DataSource newDDS = initDDS(projectCode); DDSHolder.instance().addDDS(projectCode, newDDS); } catch (IllegalArgumentException | IllegalAccessException e) { log.error("Init data source fail. projectCode:" + projectCode); return null; } } dds = DDSHolder.instance().getDDS(projectCode); try { return dds.getConnection(); } catch (SQLException e) { e.printStackTrace(); return null; } } /** * 以当前数据对象作为模板复制一份。 * * @return dds * @throws IllegalAccessException * @throws IllegalArgumentException */ private DataSource initDDS(String projectCode) throws IllegalArgumentException, IllegalAccessException { DataSource dds = new DataSource(); // 2、复制PoolConfiguration的属性 PoolProperties property = new PoolProperties(); Field[] pfields = PoolProperties.class.getDeclaredFields(); for (Field f : pfields) { f.setAccessible(true); Object value = f.get(this.getPoolProperties()); try { f.set(property, value); } catch (Exception e) { //有一些static final的属性不能修改。忽略。 log.info("Set value fail. attr name:" + f.getName()); continue; } } dds.setPoolProperties(property); // 3、设置数据库名称和IP(一般来说,端口和用户名、密码都是统一固定的) String urlFormat = this.getUrl(); String url = String.format(urlFormat, ProjectDBMgr.instance().getDBIP(projectCode), ProjectDBMgr.instance().getDBName(projectCode)); dds.setUrl(url); return dds; } }
3) 通过DDSTimer控制数据连接释放(DDSTimer.java)
package com.elon.dds.datasource; import org.apache.tomcat.jdbc.pool.DataSource; /** * 动态数据源定时器管理。长时间无访问的数据库连接关闭。 * * @author elon * @version 2018年2月25日 */ public class DDSTimer { /** * 空闲时间周期。超过这个时长没有访问的数据库连接将被释放。默认为10分钟。 */ private static long idlePeriodTime = 10 * 60 * 1000; /** * 动态数据源 */ private DataSource dds; /** * 上一次访问的时间 */ private long lastUseTime; public DDSTimer(DataSource dds) { this.dds = dds; this.lastUseTime = System.currentTimeMillis(); } /** * 更新最近访问时间 */ public void refreshTime() { lastUseTime = System.currentTimeMillis(); } /** * 检测数据连接是否超时关闭。 * * @return true-已超时关闭; false-未超时 */ public boolean checkAndClose() { if (System.currentTimeMillis() - lastUseTime > idlePeriodTime) { dds.close(); return true; } return false; } public DataSource getDds() { return dds; } }
4) 通过DDSHolder来管理不同的数据源,提供数据源的添加、查询功能(DDSHolder.java)
package com.elon.dds.datasource; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.Timer; import org.apache.tomcat.jdbc.pool.DataSource; /** * 动态数据源管理器。 * * @author elon * @version 2018年2月25日 */ public class DDSHolder { /** * 管理动态数据源列表。<工程编码,数据源> */ private Map<String, DDSTimer> ddsMap = new HashMap<String, DDSTimer>(); /** * 通过定时任务周期性清除不使用的数据源 */ private static Timer clearIdleTask = new Timer(); static { clearIdleTask.schedule(new ClearIdleTimerTask(), 5000, 60 * 1000); }; private DDSHolder() { } /* * 获取单例对象 */ public static DDSHolder instance() { return DDSHolderBuilder.instance; } /** * 添加动态数据源。 * * @param projectCode 项目编码 * @param dds dds */ public synchronized void addDDS(String projectCode, DataSource dds) { DDSTimer ddst = new DDSTimer(dds); ddsMap.put(projectCode, ddst); } /** * 查询动态数据源 * * @param projectCode 项目编码 * @return dds */ public synchronized DataSource getDDS(String projectCode) { if (ddsMap.containsKey(projectCode)) { DDSTimer ddst = ddsMap.get(projectCode); ddst.refreshTime(); return ddst.getDds(); } return null; } /** * 清除超时无人使用的数据源。 */ public synchronized void clearIdleDDS() { Iterator<Entry<String, DDSTimer>> iter = ddsMap.entrySet().iterator(); for (; iter.hasNext(); ) { Entry<String, DDSTimer> entry = iter.next(); if (entry.getValue().checkAndClose()) { iter.remove(); } } } /** * 单例构件类 * @author elon * @version 2018年2月26日 */ private static class DDSHolderBuilder { private static DDSHolder instance = new DDSHolder(); } }
5) 定时器任务ClearIdleTimerTask用于定时清除空闲的数据源(ClearIdleTimerTask.java)
package com.elon.dds.datasource; import java.util.TimerTask; /** * 清除空闲连接任务。 * * @author elon * @version 2018年2月26日 */ public class ClearIdleTimerTask extends TimerTask { @Override public void run() { DDSHolder.instance().clearIdleDDS(); } }
3. 管理项目编码与数据库IP和名称的映射关系(ProjectDBMgr.java)
package com.elon.dds.dbmgr; import java.util.HashMap; import java.util.Map; /** * 项目数据库管理。提供根据项目编码查询数据库名称和IP的接口。 * @author elon * @version 2018年2月25日 */ public class ProjectDBMgr { /** * 保存项目编码与数据名称的映射关系。这里是硬编码,实际开发中这个关系数据可以保存到redis缓存中; * 新增一个项目或者删除一个项目只需要更新缓存。到时这个类的接口只需要修改为从缓存拿数据。 */ private Map<String, String> dbNameMap = new HashMap<String, String>(); /** * 保存项目编码与数据库IP的映射关系。 */ private Map<String, String> dbIPMap = new HashMap<String, String>(); private ProjectDBMgr() { dbNameMap.put("project_001", "db_project_001"); dbNameMap.put("project_002", "db_project_002"); dbNameMap.put("project_003", "db_project_003"); dbIPMap.put("project_001", "127.0.0.1"); dbIPMap.put("project_002", "127.0.0.1"); dbIPMap.put("project_003", "127.0.0.1"); } public static ProjectDBMgr instance() { return ProjectDBMgrBuilder.instance; } // 实际开发中改为从缓存获取 public String getDBName(String projectCode) { if (dbNameMap.containsKey(projectCode)) { return dbNameMap.get(projectCode); } return ""; } //实际开发中改为从缓存中获取 public String getDBIP(String projectCode) { if (dbIPMap.containsKey(projectCode)) { return dbIPMap.get(projectCode); } return ""; } private static class ProjectDBMgrBuilder { private static ProjectDBMgr instance = new ProjectDBMgr(); } }
4. 编写数据库访问的mapper(UserMapper.java)
package com.elon.dds.mapper; import java.util.List; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Result; import org.apache.ibatis.annotations.Results; import org.apache.ibatis.annotations.Select; import com.elon.dds.model.User; /** * Mybatis映射接口定义。 * * @author elon * @version 2018年2月26日 */ @Mapper public interface UserMapper { /** * 查询所有用户数据 * @return 用户数据列表 */ @Results(value= { @Result(property="userId", column="id"), @Result(property="name", column="name"), @Result(property="age", column="age") }) @Select("select id, name, age from tbl_user") List<User> getUsers(); }
5. 定义查询对象模型(User.java)
package com.elon.dds.model; public class User { private int userId = -1; private String name = ""; private int age = -1; @Override public String toString() { return "name:" + name + "|age:" + age; } public int getUserId() { return userId; } public void setUserId(int userId) { this.userId = userId; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }
6. 定义查询数据的restful接口(WSUser.java)
package com.elon.dds.rest; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import com.elon.dds.datasource.DBIdentifier; import com.elon.dds.mapper.UserMapper; import com.elon.dds.model.User; /** * 用户数据访问接口。 * * @author elon * @version 2018年2月26日 */ @RestController @RequestMapping(value="/user") public class WSUser { @Autowired private UserMapper userMapper; /** * 查询项目中所有用户信息 * * @param projectCode 项目编码 * @return 用户列表 */ @RequestMapping(value="/v1/users", method=RequestMethod.GET) public List<User> queryUser(@RequestParam(value="projectCode", required=true) String projectCode) { DBIdentifier.setProjectCode(projectCode); return userMapper.getUsers(); } }
要求每次查询都要带上projectCode参数。
7. 编写Spring Boot App的启动代码(App.java)
package com.elon.dds; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * Hello world! * */ @SpringBootApplication public class App { public static void main( String[] args ) { System.out.println( "Hello World!" ); SpringApplication.run(App.class, args); } }
8. 在application.yml中配置数据源
其中的数据库IP和数据库名称使用%s。在执行数据操作时动态切换。
spring: datasource: url: jdbc:mysql://%s:3306/%s?useUnicode=true&characterEncoding=utf-8 username: root password: driver-class-name: com.mysql.jdbc.Driver logging: config: classpath:log4j2.xml
测试方案
1. 查询project_001的数据,正常返回
2. 查询project_002的数据,正常返回
如何通过Spring Boot配置动态数据源访问多个数据库的更多相关文章
- Spring Boot配置多数据源并实现Druid自动切换
原文:https://blog.csdn.net/acquaintanceship/article/details/75350653 Spring Boot配置多数据源配置yml文件主数据源配置从数据 ...
- spring boot 配置双数据源mysql、sqlServer
背景:原来一直都是使用mysql数据库,在application.properties 中配置数据库信息 spring.datasource.url=jdbc:mysql://xxxx/test sp ...
- spring boot 配置多数据源
https://www.jianshu.com/p/b2e53a2521fc
- spring boot 配置虚拟静态资源文件
我们实现的目的是:通过spring boot 配置静态资源访问的虚拟路径,可实现在服务器,或者在本地通过:http://ip地址:端口/资源路径/文件名 ,可直接访问文件 比如:我们本地电脑的:E: ...
- Spring配置动态数据源-读写分离和多数据源
在现在互联网系统中,随着用户量的增长,单数据源通常无法满足系统的负载要求.因此为了解决用户量增长带来的压力,在数据库层面会采用读写分离技术和数据库拆分等技术.读写分离就是就是一个Master数据库,多 ...
- Spring Boot2.x 动态数据源配置
原文链接: Spring Boot2.x 动态数据源配置 基于 Spring Boot 2.x.Spring Data JPA.druid.mysql 的动态数据源配置Demo,适合用于数据库的读写分 ...
- Spring boot配置多个Redis数据源操作实例
原文:https://www.jianshu.com/p/c79b65b253fa Spring boot配置多个Redis数据源操作实例 在SpringBoot是项目中整合了两个Redis的操作实例 ...
- spring boot 配置访问其他模块包中的mapper和xml
maven项目结构如下,这里只是简单测试demo,使用的springboot版本为2.1.3.RELEASE 1.comm模块主要是一些mybatis的mapper接口和对应的xml文件,以及数据库表 ...
- spring boot 开静态资源访问,配置视图解析器
配置视图解析器spring.mvc.view.prefix=/pages/spring.mvc.view.suffiix= spring boot 开静态资源访问application.proerti ...
随机推荐
- oracle12c各个版本对其需要的依赖包及系统参数的修改
本文来自我的github pages博客http://galengao.github.io/ 即www.gaohuirong.cn 以下是我在oracle官网上对oracle12c 各个版本的依赖包需 ...
- mysql有多条记录的单个字段想存为一个字段显示的方法
SELECT po.id,(SELECT GROUP_CONCAT(mr.member_type) as memberTypeList FROM prod_offer_member_rel mr WH ...
- 浅谈扩展欧几里得算法(exgcd)
在讲解扩展欧几里得之前我们先回顾下辗转相除法: \(gcd(a,b)=gcd(b,a\%b)\)当a%b==0的时候b即为所求最大公约数 好了切入正题: 简单地来说exgcd函数求解的是\(ax+by ...
- 【BZOJ2127】happiness
Time Limit: 1000 ms Memory Limit: 256 MB Description 高一一班的座位表是个n*m的矩阵,经过一个学期的相处,每个同学和前后左右相邻的同学互相成为 ...
- MFC使用SQLite 学习系列 二:无法容忍的数据插入效率
上一篇随笔中,介绍了,基本的使用没什么问题了,那么开始数据的插入. 一 问题--无法容忍的插入效率 代码写入基本完成,然后开始测试.起初,插入数据的时候基本上是插入每次插入9组数据,看不出来数据插入的 ...
- 最实用的Android开发学习路线分享
Android开发学习路线分享.Android发展主导移动互联发展进程,在热门行业来说,Android开发堪称火爆,但是,虽然Android有着自身种种优势,但对开发者的专业性要求也是极高,这种要求随 ...
- 给VMware的虚拟机设置静态地址
最近在VMware 上运行新版本Linux 虚拟机集群,在给每个虚拟机设置静态IP时,遇到一些挫折,新版本有些变动,故记录下来备用. Centos版本信息7.4.1708: Ubuntu版本信息17. ...
- 搜索引擎的缓存(cache)机制
什么是缓存? 在搜索领域中,所谓缓存,就是在高速内存硬件设备上为搜索引擎开辟一块存储区,来存储常见的用户查询及其结果,并采用一定的管理策略来维护缓存区内的数据.当搜索引擎再次接收到用户的查询请求时,首 ...
- 深度学习word2vec笔记之基础篇
作者为falao_beiliu. 作者:杨超链接:http://www.zhihu.com/question/21661274/answer/19331979来源:知乎著作权归作者所有.商业转载请联系 ...
- JavaScript禁止浏览器默认行为
JavaScript禁止浏览器默认行为 1.实现源码 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN&quo ...