之前写过一篇博客《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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
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) 首先增加一个数据库标识类,用于区分不同的数据库访问。
由于我们为不同的project创建了单独的数据库,所以使用项目编码作为数据库的索引。而微服务支持多线程并发的,采用线程变量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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,在其中实现数据库连接的动态切换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
|
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)
{
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控制数据连接释放(超过指定时间未使用的数据源释放)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
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来管理不同的数据源,提供数据源的添加、查询功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
|
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用于定时清除空闲的数据源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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和名称的映射关系
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
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. 定义查询对象模型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
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接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
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的启动代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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。在查询用户数据中动态切换。
1
2
3
4
5
6
7
8
|
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配置动态数据源访问多个数据库的实现代码,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对服务器之家网站的支持!
原文链接:https://www.cnblogs.com/elon/archive/2018/03/01/8486618.html