之前寫過一篇博客《Spring+Mybatis+Mysql搭建分散式資料庫訪問框架》描述如何通過Spring+Mybatis配置動態數據源訪問多個資料庫。但是之前的方案有一些限制(原博客中也描述了):只適用於資料庫數量不多且固定的情況。針對資料庫動態增加的情況無能為力。 下麵講的方案能支持資料庫動態 ...
之前寫過一篇博客《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)
1 package com.elon.dds.config; 2 3 import javax.sql.DataSource; 4 5 import org.apache.ibatis.session.SqlSessionFactory; 6 import org.mybatis.spring.SqlSessionFactoryBean; 7 import org.mybatis.spring.annotation.MapperScan; 8 import org.springframework.beans.factory.annotation.Qualifier; 9 import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder; 10 import org.springframework.boot.context.properties.ConfigurationProperties; 11 import org.springframework.context.annotation.Bean; 12 import org.springframework.context.annotation.Configuration; 13 14 import com.elon.dds.datasource.DynamicDataSource; 15 16 /** 17 * 數據源配置管理。 18 * 19 * @author elon 20 * @version 2018年2月26日 21 */ 22 @Configuration 23 @MapperScan(basePackages="com.elon.dds.mapper", value="sqlSessionFactory") 24 public class DataSourceConfig { 25 26 /** 27 * 根據配置參數創建數據源。使用派生的子類。 28 * 29 * @return 數據源 30 */ 31 @Bean(name="dataSource") 32 @ConfigurationProperties(prefix="spring.datasource") 33 public DataSource getDataSource() { 34 DataSourceBuilder builder = DataSourceBuilder.create(); 35 builder.type(DynamicDataSource.class); 36 return builder.build(); 37 } 38 39 /** 40 * 創建會話工廠。 41 * 42 * @param dataSource 數據源 43 * @return 會話工廠 44 */ 45 @Bean(name="sqlSessionFactory") 46 public SqlSessionFactory getSqlSessionFactory(@Qualifier("dataSource") DataSource dataSource) { 47 SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); 48 bean.setDataSource(dataSource); 49 50 try { 51 return bean.getObject(); 52 } catch (Exception e) { 53 e.printStackTrace(); 54 return null; 55 } 56 } 57 }
2. 定義動態數據源
1) 首先增加一個資料庫標識類,用於區分不同的資料庫(DBIdentifier.java)
由於我們為不同的project創建了單獨的資料庫,所以使用項目編碼作為資料庫的索引。而微服務支持多線程併發的,採用線程變數。
1 package com.elon.dds.datasource; 2 3 /** 4 * 資料庫標識管理類。用於區分數據源連接的不同資料庫。 5 * 6 * @author elon 7 * @version 2018-02-25 8 */ 9 public class DBIdentifier { 10 11 /** 12 * 用不同的工程編碼來區分資料庫 13 */ 14 private static ThreadLocal<String> projectCode = new ThreadLocal<String>(); 15 16 public static String getProjectCode() { 17 return projectCode.get(); 18 } 19 20 public static void setProjectCode(String code) { 21 projectCode.set(code); 22 } 23 }
2) 從DataSource派生了一個DynamicDataSource,在其中實現資料庫連接的動態切換(DynamicDataSource.java)
1 package com.elon.dds.datasource; 2 3 import java.lang.reflect.Field; 4 import java.sql.Connection; 5 import java.sql.SQLException; 6 7 import org.apache.logging.log4j.LogManager; 8 import org.apache.logging.log4j.Logger; 9 import org.apache.tomcat.jdbc.pool.DataSource; 10 import org.apache.tomcat.jdbc.pool.PoolProperties; 11 12 import com.elon.dds.dbmgr.ProjectDBMgr; 13 14 /** 15 * 定義動態數據源派生類。從基礎的DataSource派生,動態性自己實現。 16 * 17 * @author elon 18 * @version 2018-02-25 19 */ 20 public class DynamicDataSource extends DataSource { 21 22 private static Logger log = LogManager.getLogger(DynamicDataSource.class); 23 24 /** 25 * 改寫本方法是為了在請求不同工程的數據時去連接不同的資料庫。 26 */ 27 @Override 28 public Connection getConnection(){ 29 30 String projectCode = DBIdentifier.getProjectCode(); 31 32 //1、獲取數據源 33 DataSource dds = DDSHolder.instance().getDDS(projectCode); 34 35 //2、如果數據源不存在則創建 36 if (dds == null) { 37 try { 38 DataSource newDDS = initDDS(projectCode); 39 DDSHolder.instance().addDDS(projectCode, newDDS); 40 } catch (IllegalArgumentException | IllegalAccessException e) { 41 log.error("Init data source fail. projectCode:" + projectCode); 42 return null; 43 } 44 } 45 46 dds = DDSHolder.instance().getDDS(projectCode); 47 try { 48 return dds.getConnection(); 49 } catch (SQLException e) { 50 e.printStackTrace(); 51 return null; 52 } 53 } 54 55 /** 56 * 以當前數據對象作為模板複製一份。 57 * 58 * @return dds 59 * @throws IllegalAccessException 60 * @throws IllegalArgumentException 61 */ 62 private DataSource initDDS(String projectCode) throws IllegalArgumentException, IllegalAccessException { 63 64 DataSource dds = new DataSource(); 65 66 // 2、複製PoolConfiguration的屬性 67 PoolProperties property = new PoolProperties(); 68 Field[] pfields = PoolProperties.class.getDeclaredFields(); 69 for (Field f : pfields) { 70 f.setAccessible(true); 71 Object value = f.get(this.getPoolProperties()); 72 73 try 74 { 75 f.set(property, value); 76 } 77 catch (Exception e) 78 { 79 //有一些static final的屬性不能修改。忽略。 80 log.info("Set value fail. attr name:" + f.getName()); 81 continue; 82 } 83 } 84 dds.setPoolProperties(property); 85 86 // 3、設置資料庫名稱和IP(一般來說,埠和用戶名、密碼都是統一固定的) 87 String urlFormat = this.getUrl(); 88 String url = String.format(urlFormat, ProjectDBMgr.instance().getDBIP(projectCode), 89 ProjectDBMgr.instance().getDBName(projectCode)); 90 dds.setUrl(url); 91 92 return dds; 93 } 94 }
3) 通過DDSTimer控制數據連接釋放(DDSTimer.java)
1 package com.elon.dds.datasource; 2 3 import org.apache.tomcat.jdbc.pool.DataSource; 4 5 /** 6 * 動態數據源定時器管理。長時間無訪問的資料庫連接關閉。 7 * 8 * @author elon 9 * @version 2018年2月25日 10 */ 11 public class DDSTimer { 12 13 /** 14 * 空閑時間周期。超過這個時長沒有訪問的資料庫連接將被釋放。預設為10分鐘。 15 */ 16 private static long idlePeriodTime = 10 * 60 * 1000; 17 18 /** 19 * 動態數據源 20 */ 21 private DataSource dds; 22 23 /** 24 * 上一次訪問的時間 25 */ 26 private long lastUseTime; 27 28 public DDSTimer(DataSource dds) { 29 this.dds = dds; 30 this.lastUseTime = System.currentTimeMillis(); 31 } 32 33 /** 34 * 更新最近訪問時間 35 */ 36 public void refreshTime() { 37 lastUseTime = System.currentTimeMillis(); 38 } 39 40 /** 41 * 檢測數據連接是否超時關閉。 42 * 43 * @return true-已超時關閉; false-未超時 44 */ 45 public boolean checkAndClose() { 46 47 if (System.currentTimeMillis() - lastUseTime > idlePeriodTime) 48 { 49 dds.close(); 50 return true; 51 } 52 53 return false; 54 } 55 56 public DataSource getDds() { 57 return dds; 58 } 59 }
4) 通過DDSHolder來管理不同的數據源,提供數據源的添加、查詢功能(DDSHolder.java)
1 package com.elon.dds.datasource; 2 3 import java.util.HashMap; 4 import java.util.Iterator; 5 import java.util.Map; 6 import java.util.Map.Entry; 7 import java.util.Timer; 8 9 import org.apache.tomcat.jdbc.pool.DataSource; 10 11 /** 12 * 動態數據源管理器。 13 * 14 * @author elon 15 * @version 2018年2月25日 16 */ 17 public class DDSHolder { 18 19 /** 20 * 管理動態數據源列表。<工程編碼,數據源> 21 */ 22 private Map<String, DDSTimer> ddsMap = new HashMap<String, DDSTimer>(); 23 24 /** 25 * 通過定時任務周期性清除不使用的數據源 26 */ 27 private static Timer clearIdleTask = new Timer(); 28 static { 29 clearIdleTask.schedule(new ClearIdleTimerTask(), 5000, 60 * 1000); 30 }; 31 32 private DDSHolder() { 33 34 } 35 36 /* 37 * 獲取單例對象 38 */ 39 public static DDSHolder instance() { 40 return DDSHolderBuilder.instance; 41 } 42 43 /** 44 * 添加動態數據源。 45 * 46 * @param projectCode 項目編碼 47 * @param dds dds 48 */ 49 public synchronized void addDDS(String projectCode, DataSource dds) { 50 51 DDSTimer ddst = new DDSTimer(dds); 52 ddsMap.put(projectCode, ddst); 53 } 54 55 /** 56 * 查詢動態數據源 57 * 58 * @param projectCode 項目編碼 59 * @return dds 60 */ 61 public synchronized DataSource getDDS(String projectCode) { 62 63 if (ddsMap.containsKey(projectCode)) { 64 DDSTimer ddst = ddsMap.get(projectCode); 65 ddst.refreshTime(); 66 return ddst.getDds(); 67 } 68 69 return null; 70 } 71 72 /** 73 * 清除超時無人使用的數據源。 74 */ 75 public synchronized void clearIdleDDS() { 76 77 Iterator<Entry<String, DDSTimer>> iter = ddsMap.entrySet().iterator(); 78 for (; iter.hasNext(); ) { 79 80 Entry<String, DDSTimer> entry = iter.next(); 81 if (entry.getValue().checkAndClose()) 82 { 83 iter.remove(); 84 } 85 } 86 } 87 88 /** 89 * 單例構件類 90 * @author elon 91 * @version 2018年2月26日 92 */ 93 private static class DDSHolderBuilder { 94 private static DDSHolder instance = new DDSHolder(); 95 } 96 }
5) 定時器任務ClearIdleTimerTask用於定時清除空閑的數據源(ClearIdleTimerTask.java)
1 package com.elon.dds.datasource; 2 3 import java.util.TimerTask; 4 5 /** 6 * 清除空閑連接任務。 7 * 8 * @author elon 9 * @version 2018年2月26日 10 */ 11 public class ClearIdleTimerTask extends TimerTask { 12 13 @Override 14 public void run() { 15 DDSHolder.instance().clearIdleDDS(); 16 } 17 }
3. 管理項目編碼與資料庫IP和名稱的映射關係(ProjectDBMgr.java)
1 package com.elon.dds.dbmgr; 2 3 import java.util.HashMap; 4 import java.util.Map; 5 6 /** 7 * 項目資料庫管理。提供根據項目編碼查詢資料庫名稱和IP的介面。 8 * @author elon 9 * @version 2018年2月25日 10 */ 11 public class ProjectDBMgr { 12 13 /** 14 * 保存項目編碼與數據名稱的映射關係。這裡是硬編碼,實際開發中這個關係數據可以保存到redis緩存中; 15 * 新增一個項目或者刪除一個項目只需要更新緩存。到時這個類的介面只需要修改為從緩存拿數據。 16 */ 17 private Map<String, String> dbNameMap = new HashMap<String, String>(); 18 19 /** 20 * 保存項目編碼與資料庫IP的映射關係。 21 */ 22 private Map<String, String> dbIPMap = new HashMap<String, String>(); 23 24 private ProjectDBMgr() { 25 dbNameMap.put("project_001", "db_project_001"); 26 dbNameMap.put("project_002", "db_project_002"); 27 dbNameMap.put("project_003", "db_project_003"); 28 29 dbIPMap.put("project_001", "127.0.0.1"); 30 dbIPMap.put("project_002", "127.0.0.1"); 31 dbIPMap.put("project_003", "127.0.0.1"); 32 } 33 34 public static ProjectDBMgr instance() { 35 return ProjectDBMgrBuilder.instance; 36 } 37 38 // 實際開發中改為從緩存獲取 39 public String getDBName(String projectCode) { 40 if (dbNameMap.containsKey(projectCode)) { 41 return dbNameMap.get(projectCode); 42 } 43 44 return ""; 45 } 46 47 //實際開發中改為從緩存中獲取 48 public String getDBIP(String projectCode) { 49 if (dbIPMap.containsKey(projectCode)) { 50 return dbIPMap.get(projectCode); 51 } 52 53 return ""; 54 } 55 56 private static class ProjectDBMgrBuilder { 57 private static ProjectDBMgr instance = new ProjectDBMgr(); 58 } 59 }
4. 編寫資料庫訪問的mapper(UserMapper.java)
1 package com.elon.dds.mapper; 2 3 import java.util.List; 4 5 import org.apache.ibatis.annotations.Mapper; 6 import org.apache.ibatis.annotations.Result; 7 import org.apache.ibatis.annotations.Results; 8 import org.apache.ibatis.annotations.Select; 9 10 import com.elon.dds.model.User; 11 12 /** 13 * Mybatis映射介面定義。 14 * 15 * @author elon 16 * @version 2018年2月26日 17 */ 18 @Mapper 19 public interface UserMapper 20 { 21 /** 22 * 查詢所有用戶數據 23 * @return 用戶數據列表 24 */ 25 @Results(value= { 26 @Result(property="userId", column="id"), 27 @Result(property="name", column="name"), 28 @Result(property="age", column="age") 29 }) 30 @Select("select id, name, age from tbl_user") 31 List<User> getUsers(); 32 }
5. 定義查詢對象模型(User.java)
1 package com.elon.dds.model; 2 3 public class User 4 { 5 private int userId = -1; 6 7 private String name = ""; 8 9 private int age = -1; 10 11 @Override 12 public String toString() 13 { 14 return "name:" + name + "|age:" + age; 15 } 16 17 public int getUserId() 18 { 19 return userId; 20 } 21 22 public void setUserId(int userId) 23 { 24 this.userId = userId; 25 } 26 27 public String getName() 28 { 29 return name; 30 } 31 32 public void setName(String name) 33 { 34 this.name = name; 35 } 36 37 public int getAge() 38 { 39 return age; 40 } 41 42 public void setAge(int age) 43 { 44 this.age = age; 45 } 46 }
6. 定義查詢數據的restful介面(WSUser.java)
1 package com.elon.dds.rest; 2 3 import java.util.List; 4 5 import org.springframework.beans.factory.annotation.Autowired; 6 import org.springframework.web.bind.annotation.RequestMapping; 7 import org.springframework.web.bind.annotation.RequestMethod; 8 import org.springframework.web.bind.annotation.RequestParam; 9 import org.springframework.web.bind.annotation.RestController; 10 11 import com.elon.dds.datasource.DBIdentifier; 12 import com.elon.dds.mapper.UserMapper; 13 import com.elon.dds.model.User; 14 15 /** 16 * 用戶數據訪問介面。 17 * 18 * @author elon 19 * @version 2018年2月26日 20 */ 21 @RestController 22 @RequestMapping(value="/user") 23 public class WSUser { 24 25 @Autowired 26 private UserMapper userMapper; 27 28 /** 29 * 查詢項目中所有用戶信息 30 * 31 * @param projectCode 項目編碼 32 * @return 用戶列表 33 */ 34 @RequestMapping(value="/v1/users", method=RequestMethod.GET) 35 public List<User> queryUser(@RequestParam(value="projectCode", required=true) String projectCode) 36 { 37 DBIdentifier.setProjectCode(projectCode); 38 return userMapper.getUsers(); 39 } 40 }
要求每次查詢都要帶上projectCode參數。
7. 編寫Spring Boot App的啟動代碼(App.java)
1 package com.elon.dds; 2 3 import org.springframework.boot.SpringApplication; 4 import org.springframework.boot.autoconfigure.SpringBootApplication; 5 6 /** 7 * Hello world! 8 * 9 */ 10 @SpringBootApplication 11 public class App 12 { 13 public static void main( String[] args ) 14 { 15 System.out.println( "Hello World!" ); 16 SpringApplication.run(App.class, args); 17 } 18 }
8. 在application.yml中配置數據源
其中的資料庫IP和資料庫名稱使用%s。