核心關鍵點: 封裝一個DataSource, 重寫 getConnection 就可以實現
我們一步一步來看.
環境: Spring Cloud + MyBatis
MyBatis常規方式下配置資料來源: 使用Spring的Configuration
package com.cnscud.cavedemo.fundmain.config;
import com.cnscud.xpower.dbn.SimpleDBNDataSourceFactory;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
/**
* Database Config 多資料來源配置: 主資料來源.
*
* @author Felix Zhang 2021-08-02 17:30
* @version 1.0.0
*/
@Configuration
@MapperScan(basePackages = {"com.cnscud.cavedemo.fundmain.dao"},
sqlSessionFactoryRef = "sqlSessionFactoryMainDataSource")
public class MainDataSourceConfig {
//常規配置: 使用application.yml裡面的配置.
@Primary
@Bean(name = "mainDataSource")
@ConfigurationProperties("spring.datasource.main")
public DataSource mainDataSource() throws Exception {
return DataSourceBuilder.create().build();
}
@Primary
@Bean(name = "sqlSessionFactoryMainDataSource")
public SqlSessionFactory sqlSessionFactoryMainDataSource(@Qualifier("mainDataSource") DataSource mainDataSource) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
//org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
//configuration.setMapUnderscoreToCamelCase(true);
//factoryBean.setConfiguration(configuration);
factoryBean.setConfigLocation(new PathMatchingResourcePatternResolver().getResource("classpath:mybatis-config.xml"));
// 使用mainDataSource資料來源, 連線mainDataSource庫
factoryBean.setDataSource(mainDataSource);
//下邊兩句僅僅用於*.xml檔案,如果整個持久層操作不需要使用到xml檔案的話(只用註解就可以搞定),則不加
//指定entity和mapper xml的路徑
//factoryBean.setTypeAliasesPackage("com.cnscud.cavedemo.fundmain.model");
factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:com/cnscud/cavedemo/fundmain/mapper/*.xml"));
return factoryBean.getObject();
}
@Primary
@Bean
public SqlSessionTemplate sqlSessionTemplateMainDataSource(@Qualifier("sqlSessionFactoryMainDataSource") SqlSessionFactory sqlSessionTemplateMainDataSource) throws Exception {
//使用註解中配置的Factory
return new SqlSessionTemplate(sqlSessionTemplateMainDataSource);
}
@Primary
@Bean
public PlatformTransactionManager mainTransactionManager(@Qualifier("mainDataSource") DataSource prodDataSource) {
return new DataSourceTransactionManager(prodDataSource);
}
}
這裡面獲取資料來源的關鍵函式是 mainDataSource, 我們自己來實現就好了:
因為這個是一次性的工作, 所以我們無法修改DataSource的指向, 只能在DataSource內部做文章, 所以我們需要自己實現一個DataSource.
其中的步驟比較多, 我們來看看最終結果:
最終的DataSourceWrapper
它完全封裝了一個DataSource, 自己並沒有任何DataSource的功能:
package com.cnscud.xpower.dbn;
import javax.sql.DataSource;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;
/**
* Datasource wrapper, 為了方便動態建立DataSource.
*
* @author Felix Zhang 2021-08-05 14:14
* @version 1.0.0
*/
public class DynamicByZookeeperDataSourceWrapper implements DataSource {
protected SimpleDBNConnectionPool simpleDBNConnectionPool;
protected String bizName;
public DynamicByZookeeperDataSourceWrapper(SimpleDBNConnectionPool simpleDBNConnectionPool, String bizName) {
this.simpleDBNConnectionPool = simpleDBNConnectionPool;
this.bizName = bizName;
}
protected DataSource pickDataSource() throws SQLException{
return simpleDBNConnectionPool.getDataSource(bizName);
}
@Override
public Connection getConnection() throws SQLException {
return pickDataSource().getConnection();
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return pickDataSource().getConnection(username, password);
}
@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
return pickDataSource().unwrap(iface);
}
@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return pickDataSource().isWrapperFor(iface);
}
@Override
public PrintWriter getLogWriter() throws SQLException {
return pickDataSource().getLogWriter();
}
@Override
public void setLogWriter(PrintWriter out) throws SQLException {
pickDataSource().setLogWriter(out);
}
@Override
public void setLoginTimeout(int seconds) throws SQLException {
pickDataSource().setLoginTimeout(seconds);
}
@Override
public int getLoginTimeout() throws SQLException {
return pickDataSource().getLoginTimeout();
}
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
throw new SQLFeatureNotSupportedException();
}
}
SimpleDBNConnectionPool
支援多個資料來源的暫存池, 可以根據name獲取不同的資料庫DataSource例項:
這個類負責建立DataSource, 儲存在Map裡. 並且能監聽Zookeeper的變化, 一旦偵聽到變化, 就close現有的DataSource.
package com.cnscud.xpower.dbn;
import com.github.zkclient.IZkDataListener;
import com.zaxxer.hikari.HikariDataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import static java.lang.String.format;
/**
* The simple datasource pool.
*
* 根據名字存放多個資料庫的DataSource, 並且會監聽Zookeeper配置, 動態重建.
*
* @author adyliu (imxylz@gmail.com)
* @since 2011-7-27
*/
public class SimpleDBNConnectionPool {
final Logger logger = LoggerFactory.getLogger(getClass());
private Map<String, DataSource> instances = new ConcurrentHashMap<>();
private final Set<String> watcherSchema = new HashSet<String>();
public DataSource getInstance(String bizName) {
try {
return findDbInstance(bizName);
}
catch (SQLException e) {
e.printStackTrace();
}
return null;
}
public Connection getConnection(String bizName) throws SQLException {
DataSource ds = getDataSource(bizName);
return ds.getConnection();
}
public DataSource getDataSource(String bizName) throws SQLException {
return findDbInstance(bizName);
}
protected void destroyInstance(final String bizName) {
synchronized (instances) {
DataSource oldInstanceIf = instances.remove(bizName);
logger.warn(format("destoryInstance %s and %s", bizName, oldInstanceIf != null ? "close datasource" : "do nothing"));
if (oldInstanceIf != null) {
closeDataSource(oldInstanceIf);
}
}
}
protected void closeDataSource(DataSource ds) {
if (ds instanceof HikariDataSource) {
try {
((HikariDataSource) ds).close();
}
catch (Exception e) {
logger.error("Close datasource failed. ", e);
}
}
}
private DataSource createInstance(Map<String, String> dbcfg) {
return new SimpleDataSourceBuilder().buildDataSource(dbcfg);
}
private DataSource findDbInstance(final String bizName) throws SQLException {
DataSource ins = instances.get(bizName);
if (ins != null) {
return ins;
}
synchronized (instances) {// 同步操作
ins = instances.get(bizName);
if (ins != null) {
return ins;
}
boolean success = false;
try {
Map<String, String> dbcfg = SchemeNodeHelper.getInstance(bizName);
if (dbcfg == null) {
throw new SQLException("No such datasouce: " + bizName);
}
ins = createInstance(dbcfg);
//log.warn("ins put "+ins);
instances.put(bizName, ins);
if (watcherSchema.add(bizName)) {
SchemeNodeHelper.watchInstance(bizName, new IZkDataListener() {
public void handleDataDeleted(String dataPath) throws Exception {
logger.warn(dataPath + " was deleted, so destroy the bizName " + bizName);
destroyInstance(bizName);
}
public void handleDataChange(String dataPath, byte[] data) throws Exception {
logger.warn(dataPath + " was changed, so destroy the bizName " + bizName);
destroyInstance(bizName);
}
});
}
success = true;
}
catch (SQLException e) {
throw e;
}
catch (Throwable t) {
throw new SQLException("cannot build datasource for bizName: " + bizName, t);
}
finally {
if (!success) {
instances.remove(bizName);
}
}
}
return ins;
}
}
真正建立DataSource的程式碼:
package com.cnscud.xpower.dbn;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.apache.commons.lang.StringUtils;
import java.util.Map;
/**
* Hikari DataSource.
*
* 思考: 可以根據引數裡面的型別來使用不同的庫建立DataSource, 例如Druid. (預設為HikariDataSource)
*
*
* @author Felix Zhang 2021-08-05 11:14
* @version 1.0.0
*/
public class SimpleDataSourceBuilder {
public HikariDataSource buildDataSource(Map<String, String> args) {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(getUrl(args));
config.setUsername(args.get("username"));
config.setPassword(args.get("password"));
config.setDriverClassName(getDriverClassName(args));
String maximumPoolSizeKey = "maximum-pool-size";
int maximumPoolSize = 30;
if(StringUtils.isNotEmpty(args.get(maximumPoolSizeKey))){
maximumPoolSize = Integer.parseInt(args.get(maximumPoolSizeKey));
}
config.addDataSourceProperty("cachePrepStmts", "true"); //是否自定義配置,為true時下面兩個引數才生效
config.addDataSourceProperty("prepStmtCacheSize", maximumPoolSize); //連線池大小預設25,官方推薦250-500
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); //單條語句最大長度預設256,官方推薦2048
config.addDataSourceProperty("useServerPrepStmts", "true"); //新版本MySQL支援伺服器端準備,開啟能夠得到顯著效能提升
config.addDataSourceProperty("useLocalSessionState", "true");
config.addDataSourceProperty("useLocalTransactionState", "true");
config.addDataSourceProperty("rewriteBatchedStatements", "true");
config.addDataSourceProperty("cacheResultSetMetadata", "true");
config.addDataSourceProperty("cacheServerConfiguration", "true");
config.addDataSourceProperty("elideSetAutoCommits", "true");
config.addDataSourceProperty("maintainTimeStats", "false");
config.setMaximumPoolSize(maximumPoolSize); //
config.setMinimumIdle(10);//最小閒置連線數,預設為0
config.setMaxLifetime(600000);//最大生存時間
config.setConnectionTimeout(30000);//超時時間30秒
config.setIdleTimeout(60000);
config.setConnectionTestQuery("select 1");
return new HikariDataSource(config);
}
private String getDriverClassName(Map<String, String> args) {
return args.get("driver-class-name");
}
private String getUrl(Map<String, String> args) {
return args.get("jdbc-url") == null ? args.get("url"): args.get("jdbc-url");
}
}
為了方便讀取Zookeeper節點, 還有個SchemeNodeHelper:
支援兩種配置檔案的方式 json或者Properties格式:
package com.cnscud.xpower.dbn;
import com.cnscud.xpower.configcenter.ConfigCenter;
import com.cnscud.xpower.utils.Jsons;
import com.github.zkclient.IZkDataListener;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.StringReader;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
/**
* 從Zookeeper的 /xpower/dbn節點下讀取資料庫配置.
* 內容支援兩種格式: json或者properties格式.
*
* JSON格式如下:
* {
* "jdbc-url": "jdbc:mysql://127.0.0.1:3306/cavedemo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=UTC",
* "username": "dbuser",
* "password": "yourpassword",
* "driver-class-name": "com.mysql.cj.jdbc.Driver"
* }
*
* Properties格式如下:
* jdbc-url: jdbc:mysql://127.0.0.1:3306/cavedemo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=UTC
* username: dbuser
* password: password
* driver-class-name: com.mysql.cj.jdbc.Driver
*
* @author Felix Zhang
* @since 2021-8-5
*/
public class SchemeNodeHelper {
static final Logger logger = LoggerFactory.getLogger(SchemeNodeHelper.class);
//支援兩種格式: json, properties
public static Map<String, String> getInstance(final String instanceName) throws Exception {
String data = ConfigCenter.getInstance().getDataAsString("/xpower/dbn/" + instanceName);
if(StringUtils.isEmpty(data)){
return null;
}
data = data.trim();
if (data.startsWith("{")) {
//as json
Map<String, String> swap = Jsons.fromJson(data, Map.class);
Map<String, String> result = new HashMap<>();
if (swap != null) {
for (String name : swap.keySet()) {
result.put(name.toLowerCase(), swap.get(name));
}
}
return result;
}
else {
//as properties
Properties props = new Properties();
try {
props.load(new StringReader(data));
}
catch (IOException e) {
logger.error("loading global config failed", e);
}
Map<String, String> result = new HashMap<>();
for (String name : props.stringPropertyNames()) {
result.put(name.toLowerCase(), props.getProperty(name));
}
return result;
}
}
public static void watchInstance(final String bizName, final IZkDataListener listener) {
final String path = "/xpower/dbn/" + bizName;
ConfigCenter.getInstance().subscribeDataChanges(path, listener);
}
}
實際應用
最後在MyBatis專案中, 替換原有MainDataSource程式碼為:
/**
* 新增@Primary註解,設定預設資料來源,事務管理器.
* 此處使用了一個可以動態重建的DataSource, 如果Zookeeper配置改變,會動態重建.
*/
@Primary
@Bean(name = "mainDataSource")
public DataSource mainDataSource() throws Exception {
return SimpleDBNDataSourceFactory.getInstance().getDataSource("cavedemo");
}
執行專案, 發現可以連上資料庫, 並且不重啟專案的情況下, 動態修改資料庫配置, 能自動重連.
專案程式碼:
https://github.com/cnscud/xpower/tree/main/xpower-main/src/main/java/com/cnscud/xpower/dbn
其中用到的 ConfigCenter 也在這個專案裡, 也可以自己實現, 就可以脫離本專案了.