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

2018-03-01 07:49:11来源:cnblogs.com作者:Elon.Yang人点击

分享

之前写过一篇博客《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:数据库访问接口。

model:映射模型。

rest:微服务对外发布的restful接口,这里用来测试。

application.yml:配置了数据库的JDBC参数。

详细的代码实现

1. 添加数据源配置

  1 package com.elon.dds.config;  2   3    4   5 import javax.sql.DataSource;  6   7    8   9 import org.apache.ibatis.session.SqlSessionFactory; 10  11 import org.mybatis.spring.SqlSessionFactoryBean; 12  13 import org.mybatis.spring.annotation.MapperScan; 14  15 import org.springframework.beans.factory.annotation.Qualifier; 16  17 import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder; 18  19 import org.springframework.boot.context.properties.ConfigurationProperties; 20  21 import org.springframework.context.annotation.Bean; 22  23 import org.springframework.context.annotation.Configuration; 24  25   26  27 import com.elon.dds.datasource.DynamicDataSource; 28  29   30  31 /** 32  33  * 数据源配置管理。 34  35  * 36  37  * @author elon 38  39  * @version 2018年2月26日 40  41  */ 42  43 @Configuration 44  45 @MapperScan(basePackages="com.elon.dds.mapper", value="sqlSessionFactory") 46  47 public class DataSourceConfig { 48  49   50  51    /** 52  53     * 根据配置参数创建数据源。使用派生的子类。 54  55     * 56  57     * @return 数据源 58  59     */ 60  61    @Bean(name="dataSource") 62  63    @ConfigurationProperties(prefix="spring.datasource") 64  65    public DataSource getDataSource() { 66  67       DataSourceBuilder builder = DataSourceBuilder.create(); 68  69       builder.type(DynamicDataSource.class); 70  71       return builder.build(); 72  73    } 74  75    76  77    /** 78  79     * 创建会话工厂。 80  81     * 82  83     * @param dataSource 数据源 84  85     * @return 会话工厂 86  87     */ 88  89    @Bean(name="sqlSessionFactory") 90  91    public SqlSessionFactory getSqlSessionFactory(@Qualifier("dataSource") DataSource dataSource) { 92  93       SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); 94  95       bean.setDataSource(dataSource); 96  97       98  99       try {100 101          return bean.getObject();102 103       } catch (Exception e) {104 105          e.printStackTrace();106 107          return null;108 109       }110 111    }112 113 } 

2.  定义动态数据源

1)  首先增加一个数据库标识类,用于区分不同的数据库访问。

由于我们为不同的project创建了单独的数据库,所以使用项目编码作为数据库的索引。而微服务支持多线程并发的,采用线程变量。

 1 package com.elon.dds.datasource; 2  3   4  5 /** 6  7  * 数据库标识管理类。用于区分数据源连接的不同数据库。 8  9  *10 11  * @author elon12 13  * @version 2018-02-2514 15  */16 17 public class DBIdentifier {18 19   20 21    /**22 23     * 用不同的工程编码来区分数据库24 25     */26 27    private static ThreadLocal<String> projectCode = new ThreadLocal<String>();28 29  30 31    public static String getProjectCode() {32 33       return projectCode.get();34 35    }36 37  38 39    public static void setProjectCode(String code) {40 41       projectCode.set(code);42 43    }44 45 }

2)  从DataSource派生了一个DynamicDataSource,在其中实现数据库连接的动态切换

  1 import java.lang.reflect.Field;  2   3 import java.sql.Connection;  4   5 import java.sql.SQLException;  6   7    8   9 import org.apache.logging.log4j.LogManager; 10  11 import org.apache.logging.log4j.Logger; 12  13 import org.apache.tomcat.jdbc.pool.DataSource; 14  15 import org.apache.tomcat.jdbc.pool.PoolProperties; 16  17   18  19 import com.elon.dds.dbmgr.ProjectDBMgr; 20  21   22  23 /** 24  25  * 定义动态数据源派生类。从基础的DataSource派生,动态性自己实现。 26  27  * 28  29  * @author elon 30  31  * @version 2018-02-25 32  33  */ 34  35 public class DynamicDataSource extends DataSource { 36  37    38  39    private static Logger log = LogManager.getLogger(DynamicDataSource.class); 40  41    42  43    /** 44  45     * 改写本方法是为了在请求不同工程的数据时去连接不同的数据库。 46  47     */ 48  49    @Override 50  51    public Connection getConnection(){ 52  53       54  55       String projectCode = DBIdentifier.getProjectCode(); 56  57       58  59       //1、获取数据源 60  61       DataSource dds = DDSHolder.instance().getDDS(projectCode); 62  63       64  65       //2、如果数据源不存在则创建 66  67       if (dds == null) { 68  69          try { 70  71             DataSource newDDS = initDDS(projectCode); 72  73             DDSHolder.instance().addDDS(projectCode, newDDS); 74  75          } catch (IllegalArgumentException | IllegalAccessException e) { 76  77             log.error("Init data source fail. projectCode:" + projectCode); 78  79             return null; 80  81          } 82  83       } 84  85       86  87       dds = DDSHolder.instance().getDDS(projectCode); 88  89       try { 90  91          return dds.getConnection(); 92  93       } catch (SQLException e) { 94  95          e.printStackTrace(); 96  97          return null; 98  99       }100 101    }102 103   104 105    /**106 107     * 以当前数据对象作为模板复制一份。108 109     *110 111     * @return dds112 113     * @throws IllegalAccessException114 115     * @throws IllegalArgumentException116 117     */118 119    private DataSource initDDS(String projectCode) throws IllegalArgumentException, IllegalAccessException {120 121      122 123       DataSource dds = new DataSource();124 125      126 127       // 2、复制PoolConfiguration的属性128 129       PoolProperties property = new PoolProperties();130 131       Field[] pfields = PoolProperties.class.getDeclaredFields();132 133       for (Field f : pfields) {134 135          f.setAccessible(true);136 137          Object value = f.get(this.getPoolProperties());138 139         140 141          try142 143          {144 145             f.set(property, value);          146 147          }148 149          catch (Exception e)150 151          {152 153             log.info("Set value fail. attr name:" + f.getName());154 155             continue;156 157          }158 159       }160 161       dds.setPoolProperties(property);162 163  164 165       // 3、设置数据库名称和IP(一般来说,端口和用户名、密码都是统一固定的)166 167       String urlFormat = this.getUrl();168 169       String url = String.format(urlFormat, ProjectDBMgr.instance().getDBIP(projectCode),170 171             ProjectDBMgr.instance().getDBName(projectCode));172 173       dds.setUrl(url);174 175  176 177       return dds;178 179    }180 181 } 

3)  通过DDSTimer控制数据连接释放(超过指定时间未使用的数据源释放)

  1 package com.elon.dds.datasource;  2   3    4   5 import org.apache.tomcat.jdbc.pool.DataSource;  6   7    8   9 /** 10  11  * 动态数据源定时器管理。长时间无访问的数据库连接关闭。 12  13  * 14  15  * @author elon 16  17  * @version 2018年2月25日 18  19  */ 20  21 public class DDSTimer { 22  23    24  25    /** 26  27     * 空闲时间周期。超过这个时长没有访问的数据库连接将被释放。默认为10分钟。 28  29     */ 30  31    private static long idlePeriodTime = 10 * 60 * 1000; 32  33    34  35    /** 36  37     * 动态数据源 38  39     */ 40  41    private DataSource dds; 42  43    44  45    /** 46  47     * 上一次访问的时间 48  49     */ 50  51    private long lastUseTime; 52  53    54  55    public DDSTimer(DataSource dds) { 56  57       this.dds = dds; 58  59       this.lastUseTime = System.currentTimeMillis(); 60  61    } 62  63    64  65    /** 66  67     * 更新最近访问时间 68  69     */ 70  71    public void refreshTime() { 72  73       lastUseTime = System.currentTimeMillis(); 74  75    } 76  77    78  79    /** 80  81     * 检测数据连接是否超时关闭。 82  83     * 84  85     * @return true-已超时关闭; false-未超时 86  87     */ 88  89    public boolean checkAndClose() { 90  91       92  93       if (System.currentTimeMillis() - lastUseTime > idlePeriodTime) 94  95       { 96  97          dds.close(); 98  99          return true;100 101       }102 103      104 105       return false;106 107    }108 109  110 111    public DataSource getDds() {112 113       return dds;114 115    }116 117 } 

4)      增加DDSHolder来管理不同的数据源,提供数据源的添加、查询功能

  1 package com.elon.dds.datasource;  2   3    4   5 import java.util.HashMap;  6   7 import java.util.Iterator;  8   9 import java.util.Map; 10  11 import java.util.Map.Entry; 12  13 import java.util.Timer; 14  15   16  17 import org.apache.tomcat.jdbc.pool.DataSource; 18  19   20  21 /** 22  23  * 动态数据源管理器。 24  25  * 26  27  * @author elon 28  29  * @version 2018年2月25日 30  31  */ 32  33 public class DDSHolder { 34  35    36  37    /** 38  39     * 管理动态数据源列表。<工程编码,数据源> 40  41     */ 42  43    private Map<String, DDSTimer> ddsMap = new HashMap<String, DDSTimer>(); 44  45   46  47    /** 48  49     * 通过定时任务周期性清除不使用的数据源 50  51     */ 52  53    private static Timer clearIdleTask = new Timer(); 54  55    static { 56  57       clearIdleTask.schedule(new ClearIdleTimerTask(), 5000, 60 * 1000); 58  59    }; 60  61    62  63    private DDSHolder() { 64  65       66  67    } 68  69    70  71    /* 72  73     * 获取单例对象 74  75     */ 76  77    public static DDSHolder instance() { 78  79       return DDSHolderBuilder.instance; 80  81    } 82  83    84  85    /** 86  87     * 添加动态数据源。 88  89     * 90  91     * @param projectCode 项目编码 92  93     * @param dds dds 94  95     */ 96  97    public synchronized void addDDS(String projectCode, DataSource dds) { 98  99      100 101       DDSTimer ddst = new DDSTimer(dds);102 103       ddsMap.put(projectCode, ddst);104 105    }106 107   108 109    /**110 111     * 查询动态数据源112 113     *114 115     * @param projectCode 项目编码116 117     * @return dds118 119     */120 121    public synchronized DataSource getDDS(String projectCode) {122 123      124 125       if (ddsMap.containsKey(projectCode)) {126 127          DDSTimer ddst = ddsMap.get(projectCode);128 129          ddst.refreshTime();130 131          return ddst.getDds();132 133       }134 135      136 137       return null;138 139    }140 141   142 143    /**144 145     * 清除超时无人使用的数据源。146 147     */148 149    public synchronized void clearIdleDDS() {150 151      152 153       Iterator<Entry<String, DDSTimer>> iter = ddsMap.entrySet().iterator();154 155       for (; iter.hasNext(); ) {156 157         158 159          Entry<String, DDSTimer> entry = iter.next();160 161          if (entry.getValue().checkAndClose())162 163          {164 165             iter.remove();166 167          }168 169       }170 171    }172 173   174 175    /**176 177     * 单例构件类178 179     * @author elon180 181     * @version 2018年2月26日182 183     */184 185    private static class DDSHolderBuilder {186 187       private static DDSHolder instance = new DDSHolder();188 189    }190 191 } 

5)      定时器任务ClearIdleTimerTask用于定时清除空闲的数据源

 1 package com.elon.dds.datasource; 2  3   4  5 import java.util.TimerTask; 6  7   8  9 /**10 11  * 清除空闲连接任务。12 13  *14 15  * @author elon16 17  * @version 2018年2月26日18 19  */20 21 public class ClearIdleTimerTask extends TimerTask {22 23   24 25    @Override26 27    public void run() {28 29       DDSHolder.instance().clearIdleDDS();30 31    }32 33 } 

3.       管理项目编码与数据库IP和名称的映射关系

  1 package com.elon.dds.dbmgr;  2   3    4   5 import java.util.HashMap;  6   7 import java.util.Map;  8   9   10  11 /** 12  13  * 项目数据库管理。提供根据项目编码查询数据库名称和IP的接口。 14  15  * @author elon 16  17  * @version 2018年2月25日 18  19  */ 20  21 public class ProjectDBMgr { 22  23    24  25    /** 26  27     * 保存项目编码与数据名称的映射关系。这里是硬编码,实际开发中这个关系数据可以保存到redis缓存中; 28  29     * 新增一个项目或者删除一个项目只需要更新缓存。到时这个类的接口只需要修改为从缓存拿数据。 30  31     */ 32  33    private Map<String, String> dbNameMap = new HashMap<String, String>(); 34  35    36  37    /** 38  39     * 保存项目编码与数据库IP的映射关系。 40  41     */ 42  43    private Map<String, String> dbIPMap = new HashMap<String, String>(); 44  45    46  47    private ProjectDBMgr() { 48  49       dbNameMap.put("project_001", "db_project_001"); 50  51       dbNameMap.put("project_002", "db_project_002"); 52  53       dbNameMap.put("project_003", "db_project_003"); 54  55       56  57       dbIPMap.put("project_001", "127.0.0.1"); 58  59       dbIPMap.put("project_002", "127.0.0.1"); 60  61       dbIPMap.put("project_003", "127.0.0.1"); 62  63    } 64  65    66  67    public static ProjectDBMgr instance() { 68  69       return ProjectDBMgrBuilder.instance; 70  71    } 72  73    74  75    // 实际开发中改为从缓存获取 76  77    public String getDBName(String projectCode) { 78  79       if (dbNameMap.containsKey(projectCode)) { 80  81          return dbNameMap.get(projectCode); 82  83       } 84  85       86  87       return ""; 88  89    } 90  91    92  93    //实际开发中改为从缓存中获取 94  95    public String getDBIP(String projectCode) { 96  97       if (dbIPMap.containsKey(projectCode)) { 98  99          return dbIPMap.get(projectCode);100 101       }102 103      104 105       return "";106 107    }108 109   110 111    private static class ProjectDBMgrBuilder {112 113       private static ProjectDBMgr instance = new ProjectDBMgr();114 115    }116 117 } 

4.       定义数据库访问的mapper

 1 package com.elon.dds.mapper; 2  3   4  5 import java.util.List; 6  7   8  9 import org.apache.ibatis.annotations.Mapper;10 11 import org.apache.ibatis.annotations.Result;12 13 import org.apache.ibatis.annotations.Results;14 15 import org.apache.ibatis.annotations.Select;16 17  18 19 import com.elon.dds.model.User;20 21  22 23 /**24 25  * Mybatis映射接口定义。26 27  *28 29  * @author elon30 31  * @version 2018年2月26日32 33  */34 35 @Mapper36 37 public interface UserMapper38 39 {40 41    /**42 43     * 查询所有用户数据44 45     * @return 用户数据列表46 47     */48 49    @Results(value= {50 51          @Result(property="userId", column="id"),52 53          @Result(property="name", column="name"),54 55          @Result(property="age", column="age")56 57    })58 59    @Select("select id, name, age from tbl_user")60 61    List<User> getUsers();62 63 } 

5.       定义查询对象模型

 1 package com.elon.dds.model; 2  3   4  5 public class User 6  7 { 8  9    private int userId = -1;10 11  12 13    private String name = "";14 15   16 17    private int age = -1;18 19   20 21    @Override22 23    public String toString()24 25    {26 27       return "name:" + name + "|age:" + age;28 29    }30 31  32 33    public int getUserId()34 35    {36 37       return userId;38 39    }40 41  42 43    public void setUserId(int userId)44 45    {46 47       this.userId = userId;48 49    }50 51  52 53    public String getName()54 55    {56 57       return name;58 59    }60 61  62 63    public void setName(String name)64 65    {66 67       this.name = name;68 69    }70 71  72 73    public int getAge()74 75    {76 77       return age;78 79    }80 81  82 83    public void setAge(int age)84 85    {86 87       this.age = age;88 89    }90 91 } 

6.       定义查询用户数据的restful接口

 1 package com.elon.dds.rest; 2  3   4  5 import java.util.List; 6  7   8  9 import org.springframework.beans.factory.annotation.Autowired;10 11 import org.springframework.web.bind.annotation.RequestMapping;12 13 import org.springframework.web.bind.annotation.RequestMethod;14 15 import org.springframework.web.bind.annotation.RequestParam;16 17 import org.springframework.web.bind.annotation.RestController;18 19  20 21 import com.elon.dds.datasource.DBIdentifier;22 23 import com.elon.dds.mapper.UserMapper;24 25 import com.elon.dds.model.User;26 27  28 29 /**30 31  * 用户数据访问接口。32 33  *34 35  * @author elon36 37  * @version 2018年2月26日38 39  */40 41 @RestController42 43 @RequestMapping(value="/user")44 45 public class WSUser {46 47  48 49    @Autowired50 51    private UserMapper userMapper;52 53   54 55    /**56 57     * 查询项目中所有用户信息58 59     *60 61     * @param projectCode 项目编码62 63     * @return 用户列表64 65     */66 67    @RequestMapping(value="/v1/users", method=RequestMethod.GET)68 69    public List<User> queryUser(@RequestParam(value="projectCode", required=true) String projectCode)70 71    {72 73       DBIdentifier.setProjectCode(projectCode);74 75       return userMapper.getUsers();76 77    }78 79 }

要求每次查询都要带上projectCode参数。

7.       编写Spring Boot App的启动代码

 1 package com.elon.dds; 2  3   4  5 import org.springframework.boot.SpringApplication; 6  7 import org.springframework.boot.autoconfigure.SpringBootApplication; 8  9  10 11 /**12 13  * Hello world!14 15  *16 17  */18 19 @SpringBootApplication20 21 public class App22 23 {24 25     public static void main( String[] args )26 27     {28 29         System.out.println( "Hello World!" );30 31         SpringApplication.run(App.class, args);32 33     }34 35 } 

8.       在application.yml中配置数据源

其中的数据库IP和数据库名称使用%s。在查询用户数据中动态切换。

 1 spring: 2  3  datasource: 4  5   url: jdbc:mysql://%s:3306/%s?useUnicode=true&characterEncoding=utf-8 6  7   username: root 8  9   password:10 11   driver-class-name: com.mysql.jdbc.Driver12 13  14 15 logging:16 17  config: classpath:log4j2.xml

 

测试方案

1.       查询project_001的数据,正常返回

 

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

 

最新文章

123

最新摄影

微信扫一扫

第七城市微信公众平台