之前寫過一篇博客《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