一.普及知識
- 一個數據源,也就代表一個數據庫,
源
=數據的源頭 - 數據源實例:一個數據庫連接,就代表一個數據源實例對象;
- 多數據源實例:多個數據庫連接對象;
二.尋找解決辦法
- 我們的項目使用SpringBoot+Mybatis開發的領域層,默認只連接一個數據庫;
- 網上查詢大部分的做法都是多數據源之間動態切換,也就是說在配置文件中提前配置好
幾個數據庫
連接信息,自己獲取配置文件中的這些配置,然後在springBoot啓動的使用想辦法自動創建這幾個數據源實例
; - 在後續需要切換數據庫的時候,只需要指定對應的數據源key,進行動態切換即可;
- 可是我們的需求並不是這樣的,我們需要根據外部的
變量
進行動態創建數據源實例,然後在切換到該數據源上 - 對於多數據源的切換和加載,以下這篇文件講的非常到位:
基於Spring Boot實現Mybatis的多數據源切換和動態數據源加載 - 所以我的項目主要需要解決的是多數據源動態加載,當然有了動態加載,動態切換就很簡單了;
pom.xml需要添加
<!-- 引入aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- druid數據源 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.6</version>
</dependency>
三.在 application.yml 中配置多個數據庫連接信息如下:
db:
default:
#url: jdbc:mysql://localhost:3306/product_master?useSSL=false&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&autoReconnect=true
driver-class-name: com.mysql.jdbc.Driver
url-base: jdbc:mysql://
host: localhost
port: 3306
dbname: ljyun_share
url-other: ?useSSL=false&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&autoReconnect=true
username: common
password: common
#藍景商城數據庫
dbMall:
driver-class-name: com.mysql.jdbc.Driver
url-base: jdbc:mysql://
host: localhost
port: 3306
dbname: db_mall
url-other: ?useSSL=false&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&autoReconnect=true
username: common
password: common
#雲平臺私有庫
privateDB:
driver-class-name: com.mysql.jdbc.Driver
url-base: jdbc:mysql://
host: localhost
port: 3306
dbname: ljyun_{id}_merchant
url-other: ?useSSL=false&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&autoReconnect=true
username: common
password: common
四.項目目錄
多數據源-1.jpg
五.動態數據設置以及獲取,本類屬於單例;
DynamicDataSource
需要繼承AbstractRoutingDataSource
package domain.dbs;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/**
* 動態數據設置以及獲取,本類屬於單例
* @author lxf 2018-09-29
*/
@Component
public class DynamicDataSource extends AbstractRoutingDataSource {
private final Logger logger = LoggerFactory.getLogger(getClass());
//單例句柄
private static DynamicDataSource instance;
private static byte[] lock=new byte[0];
//用於存儲已實例的數據源map
private static Map<Object,Object> dataSourceMap=new HashMap<Object, Object>();
/**
* 獲取當前數據源
* @return
*/
@Override
protected Object determineCurrentLookupKey() {
logger.info("Current DataSource is [{}]", DynamicDataSourceContextHolder.getDataSourceKey());
return DynamicDataSourceContextHolder.getDataSourceKey();
}
/**
* 設置數據源
* @param targetDataSources
*/
@Override
public void setTargetDataSources(Map<Object, Object> targetDataSources) {
super.setTargetDataSources(targetDataSources);
dataSourceMap.putAll(targetDataSources);
super.afterPropertiesSet();// 必須添加該句,否則新添加數據源無法識別到
}
/**
* 獲取存儲已實例的數據源map
* @return
*/
public Map<Object, Object> getDataSourceMap() {
return dataSourceMap;
}
/**
* 單例方法
* @return
*/
public static synchronized DynamicDataSource getInstance(){
if(instance==null){
synchronized (lock){
if(instance==null){
instance=new DynamicDataSource();
}
}
}
return instance;
}
/**
* 是否存在當前key的 DataSource
* @param key
* @return 存在返回 true, 不存在返回 false
*/
public static boolean isExistDataSource(String key) {
return dataSourceMap.containsKey(key);
}
}
六.數據源配置類
DataSourceConfigurer
在tomcat啓動時觸發,在該類中生成多個數據源實例並將其注入到 ApplicationContext 中;- 該類通過使用
@Configuration
和@Bean
註解,將創建好的多數據源實例自動注入到ApplicationContext
上下中,供後期切換數據庫用;
package domain.dbs;
import com.alibaba.druid.pool.DruidDataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.boot.autoconfigure.MybatisProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* 數據源配置類,在tomcat啓動時觸發,在該類中生成多個數據源實例並將其注入到 ApplicationContext 中
* @author lxf 2018-09-27
*/
@Configuration
@EnableConfigurationProperties(MybatisProperties.class)
public class DataSourceConfigurer {
//日誌logger句柄
private final Logger logger = LoggerFactory.getLogger(getClass());
//自動注入環境類,用於獲取配置文件的屬性值
@Autowired
private Environment evn;
private MybatisProperties mybatisProperties;
public DataSourceConfigurer(MybatisProperties properties) {
this.mybatisProperties = properties;
}
/**
* 創建數據源對象
* @param dbType 數據庫類型
* @return data source
*/
private DruidDataSource createDataSource(String dbType) {
//如果不指定數據庫類型,則使用默認數據庫連接
String dbName = dbType.trim().isEmpty() ? "default" : dbType.trim();
DruidDataSource dataSource = new DruidDataSource();
String prefix = "db." + dbName +".";
String dbUrl = evn.getProperty( prefix + "url-base")
+ evn.getProperty( prefix + "host") + ":"
+ evn.getProperty( prefix + "port") + "/"
+ evn.getProperty( prefix + "dbname") + evn.getProperty( prefix + "url-other");
logger.info("+++default默認數據庫連接url = " + dbUrl);
dataSource.setUrl(dbUrl);
dataSource.setUsername(evn.getProperty( prefix + "username"));
dataSource.setPassword(evn.getProperty( prefix + "password"));
dataSource.setDriverClassName(evn.getProperty( prefix + "driver-class-name"));
return dataSource;
}
/**
* spring boot 啓動後將自定義創建好的數據源對象放到TargetDataSources中用於後續的切換數據源用
* (比如:DynamicDataSourceContextHolder.setDataSourceKey("dbMall"),手動切換到dbMall數據源
* 同時指定默認數據源連接
* @return 動態數據源對象
*/
@Bean
public DynamicDataSource dynamicDataSource() {
//獲取動態數據庫的實例(單例方式)
DynamicDataSource dynamicDataSource = DynamicDataSource.getInstance();
//創建默認數據庫連接對象
DruidDataSource defaultDataSource = createDataSource("default");
//創建db_mall數據庫連接對象
DruidDataSource mallDataSource = createDataSource("dbMall");
Map<Object,Object> map = new HashMap<>();
//自定義數據源key值,將創建好的數據源對象,賦值到targetDataSources中,用於切換數據源時指定對應key即可切換
map.put("default", defaultDataSource);
map.put("dbMall", mallDataSource);
dynamicDataSource.setTargetDataSources(map);
//設置默認數據源
dynamicDataSource.setDefaultTargetDataSource(defaultDataSource);
return dynamicDataSource;
}
/**
* 配置mybatis的sqlSession連接動態數據源
* @param dynamicDataSource
* @return
* @throws Exception
*/
@Bean
public SqlSessionFactory sqlSessionFactory(
@Qualifier("dynamicDataSource") DataSource dynamicDataSource)
throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dynamicDataSource);
bean.setMapperLocations(mybatisProperties.resolveMapperLocations());
bean.setTypeAliasesPackage(mybatisProperties.getTypeAliasesPackage());
bean.setConfiguration(mybatisProperties.getConfiguration());
return bean.getObject();
}
@Bean(name = "sqlSessionTemplate")
public SqlSessionTemplate sqlSessionTemplate(
@Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory)
throws Exception {
return new SqlSessionTemplate(sqlSessionFactory);
}
/**
* 將動態數據源添加到事務管理器中,並生成新的bean
* @return the platform transaction manager
*/
@Bean
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(dynamicDataSource());
}
}
七.通過 ThreadLocal 獲取和設置線程安全的數據源 key
DynamicDataSourceContextHolder
類的實現
package domain.dbs;
/**
* 通過 ThreadLocal 獲取和設置線程安全的數據源 key
*/
public class DynamicDataSourceContextHolder {
/**
* Maintain variable for every thread, to avoid effect other thread
*/
private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>() {
/**
* 將 default 數據源的 key 作爲默認數據源的 key
*/
// @Override
// protected String initialValue() {
// return "default";
// }
};
/**
* To switch DataSource
*
* @param key the key
*/
public static synchronized void setDataSourceKey(String key) {
contextHolder.set(key);
}
/**
* Get current DataSource
*
* @return data source key
*/
public static String getDataSourceKey() {
return contextHolder.get();
}
/**
* To set DataSource as default
*/
public static void clearDataSourceKey() {
contextHolder.remove();
}
}
八.AOP實現在DAO層做動態數據源切換(本項目沒有用到)
package domain.dbs;
/**
* 動態數據源切換的切面,切 DAO 層,通過 DAO 層方法名判斷使用哪個數據源,實現數據源切換
* 關於切面的 Order 可以可以不設,因爲 @Transactional 是最低的,取決於其他切面的設置,
* 並且在 org.springframework.core.annotation.AnnotationAwareOrderComparator 會重新排序
*
* 注意:本項目因爲是外部傳遞進來的雲編號,根據動態創建數據源實例,並且進行切換,而這種只用dao層切面的方式,
* 適用於進行多個master/slave讀寫分類用的場景,所以我們的項目用不到這種方式(我們如果使用這種方式,
* 就需要修改daoAai入參方式,在前置處理器獲取dao的方法參數,根據參數切換數據庫,這樣就需要修改dao接口,
* 以及對應mapper.xml,需要了解動態代理的知識,所以目前我們沒有使用該方式,目前我們使用的是
* 在service或controller層手動切庫)
*/
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
//@Aspect
//@Component
public class DynamicDataSourceAspect {
private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceAspect.class);
private final String[] QUERY_PREFIX = {"select"};
@Pointcut("execution( * domain.dao.impl.*.*(..))")
public void daoAspect() {
}
@Before("daoAspect()")
public void switchDataSource(JoinPoint point) {
Object[] params = point.getArgs();
System.out.println(params.toString());
String param = (String) params[0];
for (Object string:params
) {
System.out.println(string.toString());
}
System.out.println("###################################################");
System.out.println(point.getSignature().getName());
Boolean isQueryMethod = isQueryMethod(point.getSignature().getName());
//DynamicDataSourceContextHolder.setDataSourceKey("slave");
if (isQueryMethod) {
DynamicDataSourceContextHolder.setDataSourceKey("slave");
logger.info("Switch DataSource to [{}] in Method [{}]",
DynamicDataSourceContextHolder.getDataSourceKey(), point.getSignature());
}
}
@After("daoAspect())")
public void restoreDataSource(JoinPoint point) {
DynamicDataSourceContextHolder.clearDataSourceKey();
logger.info("Restore DataSource to [{}] in Method [{}]",
DynamicDataSourceContextHolder.getDataSourceKey(), point.getSignature());
}
private Boolean isQueryMethod(String methodName) {
for (String prefix : QUERY_PREFIX) {
if (methodName.startsWith(prefix)) {
return true;
}
}
return false;
}
}
九.SwitchDB手動切換數據庫類
- 在
Controller
和Service
需要切換數據庫的使用,需要使用SwitchDB.change()
方法.
package domain.dbs;
import com.alibaba.druid.pool.DruidDataSource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.transaction.PlatformTransactionManager;
import java.util.HashMap;
import java.util.Map;
/**
* 切換數據庫類
* @author lxf 2018-09-28
*/
@Configuration
@Slf4j
public class SwitchDB {
@Autowired
private Environment evn;
//私有庫數據源key
private static String ljyunDataSourceKey = "ljyun_" ;
@Autowired
DynamicDataSource dynamicDataSource;
@Autowired
private PlatformTransactionManager transactionManager;
/**
* 切換數據庫對外方法,如果私有庫id參數非0,則首先連接私有庫,否則連接其他已存在的數據源
* @param dbName 已存在的數據庫源對象
* @param ljyunId 私有庫主鍵
* @return 返回當前數據庫連接對象對應的key
*/
public String change(String dbName,int ljyunId)
{
if( ljyunId == 0){
toDB(dbName);
}else {
toYunDB(ljyunId);
}
//獲取當前連接的數據源對象的key
String currentKey = DynamicDataSourceContextHolder.getDataSourceKey();
log.info("=====當前連接的數據庫是:" + currentKey);
return currentKey;
}
/**
* 切換已存在的數據源
* @param dbName
*/
private void toDB(String dbName)
{
//如果不指定數據庫,則直接連接默認數據庫
String dbSourceKey = dbName.trim().isEmpty() ? "default" : dbName.trim();
//獲取當前連接的數據源對象的key
String currentKey = DynamicDataSourceContextHolder.getDataSourceKey();
//如果當前數據庫連接已經是想要的連接,則直接返回
if(currentKey == dbSourceKey) return;
//判斷儲存動態數據源實例的map中key值是否存在
if( DynamicDataSource.isExistDataSource(dbSourceKey) ){
DynamicDataSourceContextHolder.setDataSourceKey(dbSourceKey);
log.info("=====普通庫: "+dbName+",切換完畢");
}else {
log.info("切換普通數據庫時,數據源key=" + dbName + "不存在");
}
}
/**
* 創建新的私有庫數據源
* @param ljyunId
*/
private void toYunDB(int ljyunId){
//組合私有庫數據源對象key
String dbSourceKey = ljyunDataSourceKey+String.valueOf(ljyunId);
//獲取當前連接的數據源對象的key
String currentKey = DynamicDataSourceContextHolder.getDataSourceKey();
if(dbSourceKey == currentKey) return;
//創建私有庫數據源
createLjyunDataSource(ljyunId);
//切換到當前數據源
DynamicDataSourceContextHolder.setDataSourceKey(dbSourceKey);
log.info("=====私有庫: "+ljyunId+",切換完畢");
}
/**
* 創建私有庫數據源,並將數據源賦值到targetDataSources中,供後切庫用
* @param ljyunId
* @return
*/
private DruidDataSource createLjyunDataSource(int ljyunId){
//創建新的數據源
if(ljyunId == 0)
{
log.info("動態創建私有庫數據時,私有庫主鍵丟失");
}
String yunId = String.valueOf(ljyunId);
DruidDataSource dataSource = new DruidDataSource();
String prefix = "db.privateDB.";
String dbUrl = evn.getProperty( prefix + "url-base")
+ evn.getProperty( prefix + "host") + ":"
+ evn.getProperty( prefix + "port") + "/"
+ evn.getProperty( prefix + "dbname").replace("{id}",yunId) + evn.getProperty( prefix + "url-other");
log.info("+++創建雲平臺私有庫連接url = " + dbUrl);
dataSource.setUrl(dbUrl);
dataSource.setUsername(evn.getProperty( prefix + "username"));
dataSource.setPassword(evn.getProperty( prefix + "password"));
dataSource.setDriverClassName(evn.getProperty( prefix + "driver-class-name"));
//將創建的數據源,新增到targetDataSources中
Map<Object,Object> map = new HashMap<>();
map.put(ljyunDataSourceKey+yunId, dataSource);
DynamicDataSource.getInstance().setTargetDataSources(map);
return dataSource;
}
}
十.Service中根據外部變量手動切換數據庫,使用SwitchDB.change()
TestTransaction
實現
package domain.service.impl.exhibition;
import domain.dao.impl.ExhibitionDao;
import domain.dbs.DynamicDataSource;
import domain.dbs.DynamicDataSourceContextHolder;
import domain.dbs.SwitchDB;
import domain.domain.DomainResponse;
import domain.domain.Exhibition;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
import java.util.HashMap;
import java.util.Map;
/**
* 測試切庫後的事務類
* @author lxf 2018-09-28
*/
@Service
@Slf4j
public class TestTransaction {
@Autowired
private ExhibitionDao dao;
@Autowired
private SwitchDB switchDB;
@Autowired
DynamicDataSource dynamicDataSource;
public DomainResponse testProcess(int kaiguan, int ljyunId, String dbName){
switchDB.change(dbName,ljyunId);
//獲取當前已有的數據源實例
System.out.println("%%%%%%%%"+dynamicDataSource.getDataSourceMap());
return process(kaiguan,ljyunId,dbName);
}
/**
* 事務測試
* 注意:(1)有@Transactional註解的方法,方法內部不可以做切換數據庫操作
* (2)在同一個service其他方法調用帶@Transactional的方法,事務不起作用,(比如:在本類中使用testProcess調用process())
* 可以用其他service中調用帶@Transactional註解的方法,或在controller中調用.
* @param kaiguan
* @param ljyunId
* @param dbName
* @return
*/
//propagation 傳播行爲 isolation 隔離級別 rollbackFor 回滾規則
@Transactional(propagation = Propagation.REQUIRED,isolation = Isolation.DEFAULT,timeout=36000,rollbackFor=Exception.class)
public DomainResponse process(int kaiguan, int ljyunId, String dbName ) {
String currentKey = DynamicDataSourceContextHolder.getDataSourceKey();
log.info("=====service當前連接的數據庫是:" + currentKey);
Exhibition exhibition = new Exhibition();
exhibition.setExhibitionName("A-001-003");
//return new DomainResponse<String>(1, "新增成功", "");
int addRes = dao.insert(exhibition);
if(addRes>0 && kaiguan==1){
exhibition.setExhibitionName("B-001-002");
int addRes2 = dao.insert(exhibition);
return new DomainResponse<String>(1, "新增成功", "");
}else
{
Map<String,String> map = new HashMap<>();
String a = map.get("hello");
//log.info("-----a="+a.replace("a","b"));
//手動回滾事務
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return new DomainResponse<String>(0, "新增錯誤,事務已回滾", "");
}
}
}
十一.切庫與事務
-
需要在
DataSourceConfigurer
類中添加如下配置,讓事務管理器與動態數據源對應起來;/** * 將動態數據源添加到事務管理器中,並生成新的bean * @return the platform transaction manager */ @Bean public PlatformTransactionManager transactionManager() { return new DataSourceTransactionManager(dynamicDataSource()); } ```
轉載:
鏈接:https://www.jianshu.com/p/7f1b785cd986