之前写过一篇博客《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配置动态数据源访问多个数据库的更多相关文章

  1. Spring Boot配置多数据源并实现Druid自动切换

    原文:https://blog.csdn.net/acquaintanceship/article/details/75350653 Spring Boot配置多数据源配置yml文件主数据源配置从数据 ...

  2. spring boot 配置双数据源mysql、sqlServer

    背景:原来一直都是使用mysql数据库,在application.properties 中配置数据库信息 spring.datasource.url=jdbc:mysql://xxxx/test sp ...

  3. spring boot 配置多数据源

    https://www.jianshu.com/p/b2e53a2521fc

  4. spring boot 配置虚拟静态资源文件

    我们实现的目的是:通过spring boot 配置静态资源访问的虚拟路径,可实现在服务器,或者在本地通过:http://ip地址:端口/资源路径/文件名  ,可直接访问文件 比如:我们本地电脑的:E: ...

  5. Spring配置动态数据源-读写分离和多数据源

    在现在互联网系统中,随着用户量的增长,单数据源通常无法满足系统的负载要求.因此为了解决用户量增长带来的压力,在数据库层面会采用读写分离技术和数据库拆分等技术.读写分离就是就是一个Master数据库,多 ...

  6. Spring Boot2.x 动态数据源配置

    原文链接: Spring Boot2.x 动态数据源配置 基于 Spring Boot 2.x.Spring Data JPA.druid.mysql 的动态数据源配置Demo,适合用于数据库的读写分 ...

  7. Spring boot配置多个Redis数据源操作实例

    原文:https://www.jianshu.com/p/c79b65b253fa Spring boot配置多个Redis数据源操作实例 在SpringBoot是项目中整合了两个Redis的操作实例 ...

  8. spring boot 配置访问其他模块包中的mapper和xml

    maven项目结构如下,这里只是简单测试demo,使用的springboot版本为2.1.3.RELEASE 1.comm模块主要是一些mybatis的mapper接口和对应的xml文件,以及数据库表 ...

  9. spring boot 开静态资源访问,配置视图解析器

    配置视图解析器spring.mvc.view.prefix=/pages/spring.mvc.view.suffiix= spring boot 开静态资源访问application.proerti ...

随机推荐

  1. Qt Creator 整合 python 解释器教程

    目录 1. 前言 2.前提条件 3.步骤 3.1 新建 python文件 3.2 编写 python 代码 3.3 配置 python 解释器 3.4 执行 python file 1. 前言 Pyt ...

  2. linux dhcp 设置路由及主机名

    自动获取ipDHCP方式获取ip:dhclient [网络接口]释放通过DHCP获取的ip地址:dhclient -r [网络接口]查看网络接口 ifconfig -a(列出所有接口含禁用的) eth ...

  3. R+OCR︱借助tesseract包实现图片文本提取功能

    2016年11月,Jeroen Ooms在CRAN发布了tesseract包,实现了R语言对简单图片的文本提取.分析功能. 利用开源OCR引擎进行图片处理,目前可以识别超过100种语言,R语言可以借助 ...

  4. 在U-boot中添加以太网驱动

    当定义CONFIG_CMD_NET和CONFIG_CMD_PING,编译之后执行ping命令,告警没有找到以太网. 因此,需要打开U-boot的网络功能, u-boot-sunxi-sunxi中没有找 ...

  5. TOE(TCP/IP Offload Engine)网卡与一般网卡的区别

    TCP减压引擎,第一次听说这个名词,但是并不是一个新的概念了,若干年前听说过设备厂商在研究在FPGA之中实现TCP Stack,但是后来没有听到任何的产品出来,应该是路由设备to host的traff ...

  6. Linux 系统裁剪笔记 软盘2

    第一步:裁减内核打开终端,输入:cd /usr/src/linux2.4,然后输入make xconfig.现在编译内核正式开始了1.1 "code maturity level optio ...

  7. Eclipse部署Java Web项目到Tomcat出错

    1.今天,我打开Eclipse,准备将一个Java Web项目部署到Tomcat中, 结果弹出提示错误窗口,具体如下: (1)出错详情 Could not publish server configu ...

  8. 基于stm32的can总线彻底研究

    1.CAN总线的初始化 void can_init(void){      CAN_InitTypeDef        CAN_InitStructure;      CAN_FilterInitT ...

  9. JLINK(SEGGER)灯不亮 USB不识别固件修复、clone修改

    今天调SMT32插拔几下,JLINK竟然挂掉了网上找了这个教程,搞了半天才搞好,驱动没装好!WIN7系统,自动安装的驱动是GPS.COM10,郁闷,错误来的.应该是:atm6124.sys.要手动选择 ...

  10. 基于am3358的led跑马灯测试

    #include <sys/ioctl.h> #include<stdio.h> #include <fcntl.h> #include <sys/types ...