如何通过Spring Boot配置动态数据源访问多个数据库

时间:2022-12-13 13:17:41

之前写过一篇博客《Spring+Mybatis+Mysql搭建分布式数据库访问框架》描述如何通过Spring+Mybatis配置动态数据源访问多个数据库。但是之前的方案有一些限制(原博客中也描述了):只适用于数据库数量不多且固定的情况。针对数据库动态增加的情况无能为力。

下面讲的方案能支持数据库动态增删,数量不限。

数据库环境准备

下面以Mysql为例,先在本地建3个数据库用于测试。需要说明的是本方案不限数据库数量,支持不同的数据库部署在不同的服务器上。如图所示db_project_001、db_project_002、db_project_003。

如何通过Spring Boot配置动态数据源访问多个数据库

搭建Java后台微服务项目

创建一个Spring Boot的maven项目:

如何通过Spring Boot配置动态数据源访问多个数据库

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的数据,正常返回

如何通过Spring Boot配置动态数据源访问多个数据库

2.  查询project_002的数据,正常返回

如何通过Spring Boot配置动态数据源访问多个数据库

如何通过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&period;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. CSS换行文本溢出显示省略号,多行

    首先,div部分 <body> <div>多行的显示该如何解决呢,后面经过一番google后,我找到了chrome的一个API可以解决上面提到的需求-webkit-line-c ...

  2. oracle学习笔记系列------oracle 基本操作之表的增删改查

    --创建一个表 CREATE TABLE employee_souvc( id ), name ), gender ), birth DATE, salary ,), job ), deptno ) ...

  3. Java Hour 62 J2EE App 服务器

    目前略微瓶颈了,准备换工作. tomcat.weblogic.jboss的区别,容器的作用 Apache 是一个http 服务器. Tomcat 是一web 应用程序服务器,支持部分的j2ee. Jb ...

  4. Spring MVC---基于注解的控制器

                                     基于注解的控制器 SpringMVC是一个基于DispatcherServlet的MVC框架,每个请求最先访问的是Dispatcher ...

  5. 老李推荐:第5章1节《MonkeyRunner源码剖析》Monkey原理分析-启动运行&colon; 官方简介

    老李推荐:第5章1节<MonkeyRunner源码剖析>Monkey原理分析-启动运行: 官方简介   在MonkeyRunner的框架中,Monkey是作为一个服务来接受来自Monkey ...

  6. 你不知道的JavaScript--Item7 函数和(命名)函数表达式

    1.函数声明与函数表达式 在ECMAScript中,创建函数的最常用的两个方法是函数表达式和函数声明,两者期间的区别是有点晕,因为ECMA规范只明确了一点:函数声明必须带有标示符(Identifier ...

  7. curl传post数据流

  8. java基础----&gt&semi;Java中异常的使用(一)

    今天我们大致学习一下java中关于异常的知识.原来忍住一段时间不联系一个人,真的就不想联系了. java异常的使用 一.java异常的一些说明 .Throwable 类是Java 语言中所有错误或异常 ...

  9. CentOS6&period;5安装Cacti统计图乱码解决

    原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 .作者信息和本声明.否则将追究法律责任.http://fengwan.blog.51cto.com/508652/1430505 这个就 ...

  10. Data truncation&colon; Data too long for column

    是字符集问题引起的,用show full fields from + 表名就可以看出你的列的编码格式把它改成GBK或者GB2312.uTF-8.如果还不行的话,把你表的编码格式也改成上面的编码格式,我 ...